@sarj/eslint-plugin 2.2.0 → 2.3.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
@@ -848,14 +848,14 @@ var no_sequential_await_default = import_utils10.ESLintUtils.RuleCreator(
848
848
  const value = node[key];
849
849
  if (Array.isArray(value)) {
850
850
  for (const child of value) {
851
- if (isNode2(child) && !isLoop(child)) {
851
+ if (isNode4(child) && !isLoop(child)) {
852
852
  const found = findAwaitInScope(child);
853
853
  if (found) {
854
854
  return found;
855
855
  }
856
856
  }
857
857
  }
858
- } else if (isNode2(value) && !isLoop(value)) {
858
+ } else if (isNode4(value) && !isLoop(value)) {
859
859
  const found = findAwaitInScope(value);
860
860
  if (found) {
861
861
  return found;
@@ -864,7 +864,7 @@ var no_sequential_await_default = import_utils10.ESLintUtils.RuleCreator(
864
864
  }
865
865
  return null;
866
866
  }
867
- function isNode2(value) {
867
+ function isNode4(value) {
868
868
  return typeof value === "object" && value !== null && typeof value.type === "string";
869
869
  }
870
870
  function checkLoop(node) {
@@ -1588,13 +1588,13 @@ function getPropertyNode(objNode, propName2) {
1588
1588
  if (!objNode || objNode.type !== "ObjectExpression") return null;
1589
1589
  for (const prop of objNode.properties) {
1590
1590
  if (prop.type !== "Property") continue;
1591
- let keyName = null;
1591
+ let keyName2 = null;
1592
1592
  if (prop.key.type === "Identifier" && !prop.computed) {
1593
- keyName = prop.key.name;
1593
+ keyName2 = prop.key.name;
1594
1594
  } else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
1595
- keyName = prop.key.value;
1595
+ keyName2 = prop.key.value;
1596
1596
  }
1597
- if (keyName === propName2) {
1597
+ if (keyName2 === propName2) {
1598
1598
  if (prop.value.type === "AssignmentPattern" || prop.value.type === "ArrayPattern" || prop.value.type === "ObjectPattern") {
1599
1599
  return null;
1600
1600
  }
@@ -1936,6 +1936,1130 @@ var zod_naming_convention_default = import_utils21.ESLintUtils.RuleCreator(
1936
1936
  }
1937
1937
  });
1938
1938
 
1939
+ // src/rules/no-cors-wildcard-with-credentials.ts
1940
+ var import_utils22 = require("@typescript-eslint/utils");
1941
+ var ACAO_HEADER = "access-control-allow-origin";
1942
+ var ACAC_HEADER = "access-control-allow-credentials";
1943
+ var HEADER_SET_METHODS = /* @__PURE__ */ new Set(["setheader", "set", "append"]);
1944
+ function isTrueLiteral(node) {
1945
+ return node.type === "Literal" && node.value === true;
1946
+ }
1947
+ function isCredentialsTrueValue(node) {
1948
+ if (node.type === "Literal") {
1949
+ if (node.value === true) {
1950
+ return true;
1951
+ }
1952
+ if (typeof node.value === "string") {
1953
+ return node.value.trim().toLowerCase() === "true";
1954
+ }
1955
+ }
1956
+ return false;
1957
+ }
1958
+ function isStarLiteral(node) {
1959
+ return node.type === "Literal" && node.value === "*";
1960
+ }
1961
+ function subtreeContainsStarLiteral(node) {
1962
+ if (isStarLiteral(node)) {
1963
+ return true;
1964
+ }
1965
+ for (const key of Object.keys(node)) {
1966
+ if (key === "parent" || key === "loc" || key === "range") {
1967
+ continue;
1968
+ }
1969
+ const value = node[key];
1970
+ if (Array.isArray(value)) {
1971
+ for (const child of value) {
1972
+ if (isNode2(child) && subtreeContainsStarLiteral(child)) {
1973
+ return true;
1974
+ }
1975
+ }
1976
+ } else if (isNode2(value) && subtreeContainsStarLiteral(value)) {
1977
+ return true;
1978
+ }
1979
+ }
1980
+ return false;
1981
+ }
1982
+ function isNode2(value) {
1983
+ return typeof value === "object" && value !== null && typeof value.type === "string";
1984
+ }
1985
+ function propertyKeyName(prop) {
1986
+ if (prop.computed) {
1987
+ return void 0;
1988
+ }
1989
+ const key = prop.key;
1990
+ if (key.type === "Identifier") {
1991
+ return key.name;
1992
+ }
1993
+ if (key.type === "Literal" && typeof key.value === "string") {
1994
+ return key.value;
1995
+ }
1996
+ return void 0;
1997
+ }
1998
+ function calleeName(node) {
1999
+ const callee = node.callee;
2000
+ if (callee.type === "Identifier") {
2001
+ return callee.name;
2002
+ }
2003
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
2004
+ return callee.property.name;
2005
+ }
2006
+ return void 0;
2007
+ }
2008
+ function isCorsWildcardCredentialsCall(node) {
2009
+ const name = calleeName(node);
2010
+ if (name === void 0 || name.toLowerCase() !== "cors") {
2011
+ return false;
2012
+ }
2013
+ const options = node.arguments.find(
2014
+ (arg) => arg.type === "ObjectExpression"
2015
+ );
2016
+ if (options === void 0) {
2017
+ return false;
2018
+ }
2019
+ let hasCredentials = false;
2020
+ let hasWildcardOrigin = false;
2021
+ for (const prop of options.properties) {
2022
+ if (prop.type !== "Property") {
2023
+ continue;
2024
+ }
2025
+ const key = propertyKeyName(prop);
2026
+ if (key === "credentials" && isTrueLiteral(prop.value)) {
2027
+ hasCredentials = true;
2028
+ } else if (key === "origin" && subtreeContainsStarLiteral(prop.value)) {
2029
+ hasWildcardOrigin = true;
2030
+ }
2031
+ }
2032
+ return hasCredentials && hasWildcardOrigin;
2033
+ }
2034
+ function isWildcardCredentialsHeaderObject(node) {
2035
+ let wildcardOrigin = false;
2036
+ let credentialsTrue = false;
2037
+ for (const prop of node.properties) {
2038
+ if (prop.type !== "Property") {
2039
+ continue;
2040
+ }
2041
+ const key = propertyKeyName(prop);
2042
+ if (key === void 0) {
2043
+ continue;
2044
+ }
2045
+ const header = key.toLowerCase();
2046
+ if (header === ACAO_HEADER && isStarLiteral(prop.value)) {
2047
+ wildcardOrigin = true;
2048
+ } else if (header === ACAC_HEADER && isCredentialsTrueValue(prop.value)) {
2049
+ credentialsTrue = true;
2050
+ }
2051
+ }
2052
+ return wildcardOrigin && credentialsTrue;
2053
+ }
2054
+ function classifyHeaderSetCall(node) {
2055
+ const callee = node.callee;
2056
+ if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !HEADER_SET_METHODS.has(callee.property.name.toLowerCase())) {
2057
+ return void 0;
2058
+ }
2059
+ const [nameArg, valueArg] = node.arguments;
2060
+ if (nameArg === void 0 || valueArg === void 0 || nameArg.type !== "Literal" || typeof nameArg.value !== "string") {
2061
+ return void 0;
2062
+ }
2063
+ const header = nameArg.value.toLowerCase();
2064
+ if (header === ACAO_HEADER && isStarLiteral(valueArg)) {
2065
+ return "origin";
2066
+ }
2067
+ if (header === ACAC_HEADER && isCredentialsTrueValue(valueArg)) {
2068
+ return "credentials";
2069
+ }
2070
+ return void 0;
2071
+ }
2072
+ function enclosingScope(node) {
2073
+ let current = node.parent;
2074
+ while (current) {
2075
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
2076
+ return current;
2077
+ }
2078
+ current = current.parent;
2079
+ }
2080
+ return void 0;
2081
+ }
2082
+ var no_cors_wildcard_with_credentials_default = import_utils22.ESLintUtils.RuleCreator(
2083
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
2084
+ )({
2085
+ name: "no-cors-wildcard-with-credentials",
2086
+ meta: {
2087
+ type: "problem",
2088
+ docs: {
2089
+ description: 'Disallow CORS that reflects any Origin (`"*"`) while allowing credentials; any site could then read authenticated responses. Enumerate explicit trusted origins instead.'
2090
+ },
2091
+ schema: [],
2092
+ messages: {
2093
+ corsWildcardWithCredentials: 'CORS reflects any Origin (`"*"`) while allowing credentials \u2014 any site can read authenticated responses. Enumerate explicit trusted origins instead of using `"*"` with credentials.'
2094
+ }
2095
+ },
2096
+ defaultOptions: [],
2097
+ create(context) {
2098
+ const scopeHeaderSets = /* @__PURE__ */ new Map();
2099
+ function recordHeaderSet(node, kind) {
2100
+ const key = enclosingScope(node) ?? "module";
2101
+ let entry = scopeHeaderSets.get(key);
2102
+ if (entry === void 0) {
2103
+ entry = { originNodes: [], credentialsNodes: [] };
2104
+ scopeHeaderSets.set(key, entry);
2105
+ }
2106
+ if (kind === "origin") {
2107
+ entry.originNodes.push(node);
2108
+ } else {
2109
+ entry.credentialsNodes.push(node);
2110
+ }
2111
+ }
2112
+ return {
2113
+ NewExpression(node) {
2114
+ if (isCorsWildcardCredentialsCall(node)) {
2115
+ context.report({ node, messageId: "corsWildcardWithCredentials" });
2116
+ }
2117
+ },
2118
+ CallExpression(node) {
2119
+ if (isCorsWildcardCredentialsCall(node)) {
2120
+ context.report({ node, messageId: "corsWildcardWithCredentials" });
2121
+ return;
2122
+ }
2123
+ const kind = classifyHeaderSetCall(node);
2124
+ if (kind !== void 0) {
2125
+ recordHeaderSet(node, kind);
2126
+ }
2127
+ },
2128
+ ObjectExpression(node) {
2129
+ if (isWildcardCredentialsHeaderObject(node)) {
2130
+ context.report({ node, messageId: "corsWildcardWithCredentials" });
2131
+ }
2132
+ },
2133
+ "Program:exit"() {
2134
+ for (const { originNodes, credentialsNodes } of scopeHeaderSets.values()) {
2135
+ if (originNodes.length > 0 && credentialsNodes.length > 0) {
2136
+ for (const node of originNodes) {
2137
+ context.report({
2138
+ node,
2139
+ messageId: "corsWildcardWithCredentials"
2140
+ });
2141
+ }
2142
+ }
2143
+ }
2144
+ }
2145
+ };
2146
+ }
2147
+ });
2148
+
2149
+ // src/rules/no-fat-try-blocks.ts
2150
+ var import_utils23 = require("@typescript-eslint/utils");
2151
+ var MAX_TRY_BODY_STATEMENTS = 3;
2152
+ var NESTED_FUNCTION_TYPES = /* @__PURE__ */ new Set([
2153
+ import_utils23.AST_NODE_TYPES.FunctionDeclaration,
2154
+ import_utils23.AST_NODE_TYPES.FunctionExpression,
2155
+ import_utils23.AST_NODE_TYPES.ArrowFunctionExpression
2156
+ ]);
2157
+ var PURE_METHODS = /* @__PURE__ */ new Set([
2158
+ "map",
2159
+ "filter",
2160
+ "forEach",
2161
+ "reduce",
2162
+ "reduceRight",
2163
+ "find",
2164
+ "findIndex",
2165
+ "findLast",
2166
+ "findLastIndex",
2167
+ "some",
2168
+ "every",
2169
+ "push",
2170
+ "pop",
2171
+ "shift",
2172
+ "unshift",
2173
+ "slice",
2174
+ "splice",
2175
+ "concat",
2176
+ "flat",
2177
+ "flatMap",
2178
+ "join",
2179
+ "reverse",
2180
+ "sort",
2181
+ "fill",
2182
+ "includes",
2183
+ "indexOf",
2184
+ "lastIndexOf",
2185
+ "at",
2186
+ "keys",
2187
+ "values",
2188
+ "entries",
2189
+ "has",
2190
+ "get",
2191
+ "set",
2192
+ "add",
2193
+ "delete",
2194
+ "clear",
2195
+ "toString",
2196
+ "toLocaleString",
2197
+ "valueOf",
2198
+ "charAt",
2199
+ "charCodeAt",
2200
+ "codePointAt",
2201
+ "split",
2202
+ "padStart",
2203
+ "padEnd",
2204
+ "repeat",
2205
+ "trim",
2206
+ "trimStart",
2207
+ "trimEnd",
2208
+ "toUpperCase",
2209
+ "toLowerCase",
2210
+ "toFixed",
2211
+ "toPrecision",
2212
+ "startsWith",
2213
+ "endsWith"
2214
+ ]);
2215
+ var PURE_NAMESPACES = /* @__PURE__ */ new Set([
2216
+ "Object",
2217
+ "Array",
2218
+ "Math",
2219
+ "JSON",
2220
+ "Number",
2221
+ "String",
2222
+ "Boolean",
2223
+ "console"
2224
+ ]);
2225
+ var PURE_CONSTRUCTORS = /* @__PURE__ */ new Set([
2226
+ "Map",
2227
+ "Set",
2228
+ "WeakMap",
2229
+ "WeakSet",
2230
+ "Date",
2231
+ "Error",
2232
+ "TypeError",
2233
+ "RangeError",
2234
+ "Array",
2235
+ "Object",
2236
+ "Headers",
2237
+ "URLSearchParams",
2238
+ "FormData"
2239
+ ]);
2240
+ function isNode3(value) {
2241
+ return typeof value === "object" && value !== null && typeof value.type === "string";
2242
+ }
2243
+ function isPureCall(node) {
2244
+ const callee = node.callee;
2245
+ if (callee.type !== import_utils23.AST_NODE_TYPES.MemberExpression) {
2246
+ return false;
2247
+ }
2248
+ const property = callee.property;
2249
+ if (property.type !== import_utils23.AST_NODE_TYPES.Identifier) {
2250
+ return false;
2251
+ }
2252
+ if (callee.object.type === import_utils23.AST_NODE_TYPES.Identifier && PURE_NAMESPACES.has(callee.object.name)) {
2253
+ return true;
2254
+ }
2255
+ return PURE_METHODS.has(property.name);
2256
+ }
2257
+ function isPureNew(node) {
2258
+ return node.callee.type === import_utils23.AST_NODE_TYPES.Identifier && PURE_CONSTRUCTORS.has(node.callee.name);
2259
+ }
2260
+ function subtreeMatches(stmt, predicate) {
2261
+ let found = false;
2262
+ const visit = (current) => {
2263
+ if (found) {
2264
+ return;
2265
+ }
2266
+ if (predicate(current)) {
2267
+ found = true;
2268
+ return;
2269
+ }
2270
+ for (const key of Object.keys(current)) {
2271
+ if (key === "parent") {
2272
+ continue;
2273
+ }
2274
+ if (NESTED_FUNCTION_TYPES.has(current.type) && key === "body") {
2275
+ continue;
2276
+ }
2277
+ const value = current[key];
2278
+ if (Array.isArray(value)) {
2279
+ for (const child of value) {
2280
+ if (isNode3(child)) {
2281
+ visit(child);
2282
+ }
2283
+ }
2284
+ } else if (isNode3(value)) {
2285
+ visit(value);
2286
+ }
2287
+ if (found) {
2288
+ return;
2289
+ }
2290
+ }
2291
+ };
2292
+ visit(stmt);
2293
+ return found;
2294
+ }
2295
+ var hasAwait = (stmt) => subtreeMatches(stmt, (n) => n.type === import_utils23.AST_NODE_TYPES.AwaitExpression);
2296
+ var hasThrowingCallOrNew = (stmt) => subtreeMatches(
2297
+ stmt,
2298
+ (n) => n.type === import_utils23.AST_NODE_TYPES.CallExpression && !isPureCall(n) || n.type === import_utils23.AST_NODE_TYPES.NewExpression && !isPureNew(n)
2299
+ );
2300
+ function unwrap2(expr) {
2301
+ let current = expr;
2302
+ while (current.type === import_utils23.AST_NODE_TYPES.ChainExpression || current.type === import_utils23.AST_NODE_TYPES.TSNonNullExpression) {
2303
+ current = current.expression;
2304
+ }
2305
+ return current;
2306
+ }
2307
+ function canThrow(stmt) {
2308
+ if (hasAwait(stmt)) {
2309
+ return true;
2310
+ }
2311
+ if (stmt.type === import_utils23.AST_NODE_TYPES.ExpressionStatement && unwrap2(stmt.expression).type === import_utils23.AST_NODE_TYPES.CallExpression) {
2312
+ return false;
2313
+ }
2314
+ return hasThrowingCallOrNew(stmt);
2315
+ }
2316
+ function handlerRethrows(handler) {
2317
+ if (handler === null) {
2318
+ return false;
2319
+ }
2320
+ const body = handler.body.body;
2321
+ const last = body[body.length - 1];
2322
+ return last !== void 0 && last.type === import_utils23.AST_NODE_TYPES.ThrowStatement;
2323
+ }
2324
+ var no_fat_try_blocks_default = import_utils23.ESLintUtils.RuleCreator(
2325
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
2326
+ )({
2327
+ name: "no-fat-try-blocks",
2328
+ meta: {
2329
+ type: "problem",
2330
+ docs: {
2331
+ description: "Disallow `try` blocks with more than three top-level statements that can throw \u2014 isolate the throwing statement and move non-throwing work outside."
2332
+ },
2333
+ schema: [],
2334
+ messages: {
2335
+ fatTryBlock: "This `try` block has {{count}} statements that can throw (max {{max}}). Isolate the throwing statement(s); move non-throwing work outside the `try`."
2336
+ }
2337
+ },
2338
+ defaultOptions: [],
2339
+ create(context) {
2340
+ const sourceCode = context.sourceCode;
2341
+ return {
2342
+ TryStatement(node) {
2343
+ if (node.finalizer !== null) {
2344
+ return;
2345
+ }
2346
+ if (handlerRethrows(node.handler)) {
2347
+ return;
2348
+ }
2349
+ const count = node.block.body.filter(canThrow).length;
2350
+ if (count <= MAX_TRY_BODY_STATEMENTS) {
2351
+ return;
2352
+ }
2353
+ const tryKeyword = sourceCode.getFirstToken(node);
2354
+ context.report({
2355
+ node: tryKeyword ?? node,
2356
+ messageId: "fatTryBlock",
2357
+ data: { count, max: MAX_TRY_BODY_STATEMENTS }
2358
+ });
2359
+ }
2360
+ };
2361
+ }
2362
+ });
2363
+
2364
+ // src/rules/no-secret-in-log.ts
2365
+ var import_utils24 = require("@typescript-eslint/utils");
2366
+ var LOG_METHODS = /* @__PURE__ */ new Set([
2367
+ "debug",
2368
+ "info",
2369
+ "warn",
2370
+ "warning",
2371
+ "error",
2372
+ "exception",
2373
+ "critical",
2374
+ "trace",
2375
+ "log",
2376
+ "fatal",
2377
+ "success"
2378
+ ]);
2379
+ var LOGGER_NAMES = /* @__PURE__ */ new Set([
2380
+ "logger",
2381
+ "log",
2382
+ "logging",
2383
+ "loguru",
2384
+ "console",
2385
+ "_logger",
2386
+ "_log"
2387
+ ]);
2388
+ var LOGGER_FACTORIES = /* @__PURE__ */ new Set(["getlogger", "get_logger"]);
2389
+ var SECRET_WORDS = /* @__PURE__ */ new Set([
2390
+ "token",
2391
+ "secret",
2392
+ "password",
2393
+ "passwd",
2394
+ "jwt",
2395
+ "secrets",
2396
+ "passwords",
2397
+ "credential",
2398
+ "credentials",
2399
+ "authorization",
2400
+ "signature",
2401
+ "hmac",
2402
+ "digest",
2403
+ "hash",
2404
+ "apikey"
2405
+ ]);
2406
+ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
2407
+ "count",
2408
+ "counts",
2409
+ "budget",
2410
+ "limit",
2411
+ "limits",
2412
+ "id",
2413
+ "ids",
2414
+ "enabled",
2415
+ "disabled",
2416
+ "flag",
2417
+ "flags",
2418
+ "present",
2419
+ "set",
2420
+ "unset",
2421
+ "configured",
2422
+ "missing",
2423
+ "required",
2424
+ "valid",
2425
+ "invalid",
2426
+ "exists",
2427
+ "type",
2428
+ "types"
2429
+ ]);
2430
+ var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
2431
+ var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
2432
+ var CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
2433
+ var SEGMENT_RE = /[^A-Za-z0-9]+/;
2434
+ function tokenize(identifier) {
2435
+ const tokens = [];
2436
+ for (const segment of identifier.split(SEGMENT_RE)) {
2437
+ if (!segment) {
2438
+ continue;
2439
+ }
2440
+ tokens.push(segment.toLowerCase());
2441
+ for (const part of segment.match(CAMEL_RE) ?? []) {
2442
+ tokens.push(part.toLowerCase());
2443
+ }
2444
+ }
2445
+ return tokens;
2446
+ }
2447
+ function hasApiKey(tokens) {
2448
+ for (let i = 0; i + 1 < tokens.length; i++) {
2449
+ if (tokens[i] === "api" && tokens[i + 1] === "key") {
2450
+ return true;
2451
+ }
2452
+ }
2453
+ return false;
2454
+ }
2455
+ function isSecretName(identifier) {
2456
+ const tokens = tokenize(identifier);
2457
+ const last = tokens.at(-1);
2458
+ if (last !== void 0 && INNOCUOUS_WORDS.has(last)) {
2459
+ return false;
2460
+ }
2461
+ if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
2462
+ return true;
2463
+ }
2464
+ return hasApiKey(tokens);
2465
+ }
2466
+ function isSecretKeyword(name) {
2467
+ if (REDACTION_RE.test(name)) {
2468
+ return false;
2469
+ }
2470
+ if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
2471
+ return false;
2472
+ }
2473
+ return isSecretName(name);
2474
+ }
2475
+ function isLoggerExpr(expr) {
2476
+ switch (expr.type) {
2477
+ case "Identifier":
2478
+ return LOGGER_NAMES.has(expr.name.toLowerCase());
2479
+ case "MemberExpression": {
2480
+ const { property, object } = expr;
2481
+ if (!expr.computed && property.type === "Identifier") {
2482
+ const lowered = property.name.toLowerCase();
2483
+ if (LOGGER_NAMES.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
2484
+ return true;
2485
+ }
2486
+ }
2487
+ return isLoggerExpr(object);
2488
+ }
2489
+ case "CallExpression": {
2490
+ const callee = expr.callee;
2491
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
2492
+ return true;
2493
+ }
2494
+ if (callee.type !== "Super") {
2495
+ return isLoggerExpr(callee);
2496
+ }
2497
+ return false;
2498
+ }
2499
+ default:
2500
+ return false;
2501
+ }
2502
+ }
2503
+ function propertyKeyName2(prop) {
2504
+ if (prop.computed) {
2505
+ return null;
2506
+ }
2507
+ if (prop.key.type === "Identifier") {
2508
+ return prop.key.name;
2509
+ }
2510
+ if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
2511
+ return prop.key.value;
2512
+ }
2513
+ return null;
2514
+ }
2515
+ var no_secret_in_log_default = import_utils24.ESLintUtils.RuleCreator(
2516
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
2517
+ )({
2518
+ name: "no-secret-in-log",
2519
+ meta: {
2520
+ type: "problem",
2521
+ docs: {
2522
+ description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
2523
+ },
2524
+ schema: [],
2525
+ messages: {
2526
+ noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it."
2527
+ }
2528
+ },
2529
+ defaultOptions: [],
2530
+ create(context) {
2531
+ return {
2532
+ CallExpression(node) {
2533
+ const callee = node.callee;
2534
+ if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !LOG_METHODS.has(callee.property.name)) {
2535
+ return;
2536
+ }
2537
+ if (!isLoggerExpr(callee.object)) {
2538
+ return;
2539
+ }
2540
+ for (const arg of node.arguments) {
2541
+ if (arg.type === "Identifier") {
2542
+ if (isSecretKeyword(arg.name)) {
2543
+ context.report({
2544
+ node: arg,
2545
+ messageId: "noSecretInLog",
2546
+ data: { name: arg.name }
2547
+ });
2548
+ }
2549
+ continue;
2550
+ }
2551
+ if (arg.type === "ObjectExpression") {
2552
+ for (const prop of arg.properties) {
2553
+ if (prop.type !== "Property") {
2554
+ continue;
2555
+ }
2556
+ const keyName2 = propertyKeyName2(prop);
2557
+ if (keyName2 !== null && isSecretKeyword(keyName2)) {
2558
+ context.report({
2559
+ node: prop,
2560
+ messageId: "noSecretInLog",
2561
+ data: { name: keyName2 }
2562
+ });
2563
+ }
2564
+ }
2565
+ }
2566
+ }
2567
+ }
2568
+ };
2569
+ }
2570
+ });
2571
+
2572
+ // src/rules/no-template-literal-in-log.ts
2573
+ var import_utils25 = require("@typescript-eslint/utils");
2574
+ var LOG_METHODS2 = /* @__PURE__ */ new Set([
2575
+ "debug",
2576
+ "info",
2577
+ "warn",
2578
+ "warning",
2579
+ "error",
2580
+ "exception",
2581
+ "critical",
2582
+ "trace",
2583
+ "log",
2584
+ "fatal",
2585
+ "success"
2586
+ ]);
2587
+ var LOGGER_NAMES2 = /* @__PURE__ */ new Set([
2588
+ "console",
2589
+ "logger",
2590
+ "log",
2591
+ "_log",
2592
+ "_logger"
2593
+ ]);
2594
+ var LOGGER_FACTORIES2 = /* @__PURE__ */ new Set([
2595
+ "getlogger",
2596
+ "createlogger"
2597
+ ]);
2598
+ function looksLikeLogger(node) {
2599
+ switch (node.type) {
2600
+ case "Identifier": {
2601
+ const name = node.name.toLowerCase();
2602
+ return LOGGER_NAMES2.has(name) || LOGGER_FACTORIES2.has(name);
2603
+ }
2604
+ case "MemberExpression": {
2605
+ if (!node.computed && node.property.type === "Identifier") {
2606
+ const prop = node.property.name.toLowerCase();
2607
+ if (LOGGER_NAMES2.has(prop) || LOGGER_FACTORIES2.has(prop)) {
2608
+ return true;
2609
+ }
2610
+ }
2611
+ return looksLikeLogger(node.object);
2612
+ }
2613
+ case "CallExpression":
2614
+ return looksLikeLogger(node.callee);
2615
+ default:
2616
+ return false;
2617
+ }
2618
+ }
2619
+ function findInterpolatingTemplate(node) {
2620
+ if (node.type === "TemplateLiteral") {
2621
+ return node.expressions.length > 0 ? node : null;
2622
+ }
2623
+ if (node.type === "BinaryExpression" && node.operator === "+") {
2624
+ return findInterpolatingTemplate(node.left) ?? findInterpolatingTemplate(node.right);
2625
+ }
2626
+ return null;
2627
+ }
2628
+ function messageArg(node, method, receiver) {
2629
+ const levelFirst = method === "log" && !isConsoleReceiver(receiver);
2630
+ const arg = node.arguments[levelFirst ? 1 : 0];
2631
+ if (arg === void 0 || arg.type === "SpreadElement") {
2632
+ return null;
2633
+ }
2634
+ return arg;
2635
+ }
2636
+ function isConsoleReceiver(node) {
2637
+ return node.type === "Identifier" && node.name === "console";
2638
+ }
2639
+ var no_template_literal_in_log_default = import_utils25.ESLintUtils.RuleCreator(
2640
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
2641
+ )({
2642
+ name: "no-template-literal-in-log",
2643
+ meta: {
2644
+ type: "problem",
2645
+ docs: {
2646
+ description: "Disallow an interpolating template literal as a logging message \u2014 pass variables as structured fields so logs stay filterable and templates stay constant."
2647
+ },
2648
+ schema: [],
2649
+ messages: {
2650
+ noTemplateLiteralInLog: "Interpolating template literal as a logging message \u2014 pass variables as structured fields (logger.info('msg', { key })) instead."
2651
+ }
2652
+ },
2653
+ defaultOptions: [],
2654
+ create(context) {
2655
+ return {
2656
+ CallExpression(node) {
2657
+ const callee = node.callee;
2658
+ if (callee.type !== "MemberExpression" || callee.computed) {
2659
+ return;
2660
+ }
2661
+ if (callee.property.type !== "Identifier") {
2662
+ return;
2663
+ }
2664
+ const method = callee.property.name;
2665
+ if (!LOG_METHODS2.has(method)) {
2666
+ return;
2667
+ }
2668
+ if (!looksLikeLogger(callee.object)) {
2669
+ return;
2670
+ }
2671
+ const arg = messageArg(node, method, callee.object);
2672
+ if (arg === null) {
2673
+ return;
2674
+ }
2675
+ if (findInterpolatingTemplate(arg) !== null) {
2676
+ context.report({ node, messageId: "noTemplateLiteralInLog" });
2677
+ }
2678
+ }
2679
+ };
2680
+ }
2681
+ });
2682
+
2683
+ // src/rules/prefer-string-literal-union.ts
2684
+ var import_utils26 = require("@typescript-eslint/utils");
2685
+ var CHOICE_TOKENS = /* @__PURE__ */ new Set([
2686
+ "status",
2687
+ "state",
2688
+ "kind",
2689
+ "role",
2690
+ "priority",
2691
+ "severity",
2692
+ "direction",
2693
+ "tier",
2694
+ "stage",
2695
+ "type",
2696
+ "mode",
2697
+ "level"
2698
+ ]);
2699
+ var LOWER_TOKEN_RE = /^[a-z][a-z0-9_-]{0,30}$/;
2700
+ var MIN_CLUSTER_SIZE = 2;
2701
+ var IGNORE_PATTERNS = [
2702
+ /[\\/]generated[\\/]/,
2703
+ /\.gen\.tsx?$/,
2704
+ /\.generated\.tsx?$/,
2705
+ /\.d\.ts$/
2706
+ ];
2707
+ function isIgnoredFile(filename, sourceText) {
2708
+ if (IGNORE_PATTERNS.some((re) => re.test(filename))) {
2709
+ return true;
2710
+ }
2711
+ return /@generated\b/.test(sourceText.slice(0, 1024));
2712
+ }
2713
+ function lastWord(name) {
2714
+ const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\s]+/).filter((w) => w.length > 0);
2715
+ const last = words[words.length - 1] ?? name;
2716
+ return last.toLowerCase();
2717
+ }
2718
+ function isChoiceLikeName(name) {
2719
+ return CHOICE_TOKENS.has(lastWord(name));
2720
+ }
2721
+ function keyName(key) {
2722
+ if (key.type === import_utils26.AST_NODE_TYPES.Identifier) {
2723
+ return key.name;
2724
+ }
2725
+ if (key.type === import_utils26.AST_NODE_TYPES.Literal && typeof key.value === "string") {
2726
+ return key.value;
2727
+ }
2728
+ return null;
2729
+ }
2730
+ function isStringLiteralUnion(node) {
2731
+ if (node?.type !== import_utils26.AST_NODE_TYPES.TSUnionType) {
2732
+ return false;
2733
+ }
2734
+ const stringMembers = node.types.filter(
2735
+ (t) => t.type === import_utils26.AST_NODE_TYPES.TSLiteralType && t.literal.type === import_utils26.AST_NODE_TYPES.Literal && typeof t.literal.value === "string"
2736
+ );
2737
+ return stringMembers.length >= MIN_CLUSTER_SIZE;
2738
+ }
2739
+ function refKey(node) {
2740
+ if (node.type === import_utils26.AST_NODE_TYPES.Identifier) {
2741
+ return node.name;
2742
+ }
2743
+ if (node.type === import_utils26.AST_NODE_TYPES.MemberExpression && !node.computed) {
2744
+ const inner = refKey(node.object);
2745
+ if (inner === null || node.property.type !== import_utils26.AST_NODE_TYPES.Identifier) {
2746
+ return null;
2747
+ }
2748
+ return `${inner}.${node.property.name}`;
2749
+ }
2750
+ return null;
2751
+ }
2752
+ function strLiteral(node) {
2753
+ if (node.type === import_utils26.AST_NODE_TYPES.Literal && typeof node.value === "string") {
2754
+ return node.value;
2755
+ }
2756
+ return null;
2757
+ }
2758
+ var prefer_string_literal_union_default = import_utils26.ESLintUtils.RuleCreator(
2759
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
2760
+ )({
2761
+ name: "prefer-string-literal-union",
2762
+ meta: {
2763
+ type: "suggestion",
2764
+ docs: {
2765
+ description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
2766
+ },
2767
+ schema: [],
2768
+ messages: {
2769
+ bareChoiceField: '`{{name}}: string` looks like a choice field \u2014 prefer a string-literal union type (e.g. `type X = "a" | "b"`). Enums are banned by `no-enum`; use a union.',
2770
+ comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
2771
+ }
2772
+ },
2773
+ defaultOptions: [],
2774
+ create(context) {
2775
+ const filename = context.filename;
2776
+ const sourceText = context.sourceCode.getText();
2777
+ if (isIgnoredFile(filename, sourceText)) {
2778
+ return {};
2779
+ }
2780
+ const scopeStack = [];
2781
+ const validClusters = [];
2782
+ const bareChoiceProps = [];
2783
+ const containersWithUnion = /* @__PURE__ */ new Set();
2784
+ function pushScope() {
2785
+ scopeStack.push(/* @__PURE__ */ new Map());
2786
+ }
2787
+ function popScope() {
2788
+ const clusters = scopeStack.pop();
2789
+ if (clusters === void 0) {
2790
+ return;
2791
+ }
2792
+ for (const entry of clusters.values()) {
2793
+ if (entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE) {
2794
+ validClusters.push(entry.node);
2795
+ }
2796
+ }
2797
+ }
2798
+ function accumulate(key, literals, node) {
2799
+ const scope = scopeStack[scopeStack.length - 1];
2800
+ if (scope === void 0) {
2801
+ return;
2802
+ }
2803
+ const allTokens = literals.every((lit) => LOWER_TOKEN_RE.test(lit));
2804
+ const existing = scope.get(key);
2805
+ if (existing === void 0) {
2806
+ scope.set(key, {
2807
+ node,
2808
+ literals: new Set(literals),
2809
+ allTokens
2810
+ });
2811
+ return;
2812
+ }
2813
+ for (const lit of literals) {
2814
+ existing.literals.add(lit);
2815
+ }
2816
+ existing.allTokens = existing.allTokens && allTokens;
2817
+ }
2818
+ function collectProperty(key, typeNode, container, node) {
2819
+ if (isStringLiteralUnion(typeNode)) {
2820
+ containersWithUnion.add(container);
2821
+ return;
2822
+ }
2823
+ if (typeNode?.type !== import_utils26.AST_NODE_TYPES.TSStringKeyword) {
2824
+ return;
2825
+ }
2826
+ const name = keyName(key);
2827
+ if (name === null || !isChoiceLikeName(name)) {
2828
+ return;
2829
+ }
2830
+ bareChoiceProps.push({ name, container, node });
2831
+ }
2832
+ return {
2833
+ FunctionDeclaration: pushScope,
2834
+ "FunctionDeclaration:exit": popScope,
2835
+ FunctionExpression: pushScope,
2836
+ "FunctionExpression:exit": popScope,
2837
+ ArrowFunctionExpression: pushScope,
2838
+ "ArrowFunctionExpression:exit": popScope,
2839
+ BinaryExpression(node) {
2840
+ if (node.operator !== "===" && node.operator !== "!==" && node.operator !== "==" && node.operator !== "!=") {
2841
+ return;
2842
+ }
2843
+ const leftKey = refKey(node.left);
2844
+ const rightLit = strLiteral(node.right);
2845
+ const rightKey = refKey(node.right);
2846
+ const leftLit = strLiteral(node.left);
2847
+ if (leftKey !== null && rightLit !== null) {
2848
+ accumulate(leftKey, [rightLit], node);
2849
+ } else if (rightKey !== null && leftLit !== null) {
2850
+ accumulate(rightKey, [leftLit], node);
2851
+ }
2852
+ },
2853
+ SwitchStatement(node) {
2854
+ const key = refKey(node.discriminant);
2855
+ if (key === null) {
2856
+ return;
2857
+ }
2858
+ const literals = [];
2859
+ for (const c of node.cases) {
2860
+ if (c.test !== null) {
2861
+ const lit = strLiteral(c.test);
2862
+ if (lit !== null) {
2863
+ literals.push(lit);
2864
+ }
2865
+ }
2866
+ }
2867
+ if (literals.length > 0) {
2868
+ accumulate(key, literals, node);
2869
+ }
2870
+ },
2871
+ TSPropertySignature(node) {
2872
+ collectProperty(
2873
+ node.key,
2874
+ node.typeAnnotation?.typeAnnotation,
2875
+ node.parent,
2876
+ node
2877
+ );
2878
+ },
2879
+ PropertyDefinition(node) {
2880
+ collectProperty(
2881
+ node.key,
2882
+ node.typeAnnotation?.typeAnnotation,
2883
+ node.parent,
2884
+ node
2885
+ );
2886
+ },
2887
+ "Program:exit"() {
2888
+ for (const clusterNode of validClusters) {
2889
+ context.report({
2890
+ node: clusterNode,
2891
+ messageId: "comparisonCluster",
2892
+ data: { key: refKeyText(clusterNode) }
2893
+ });
2894
+ }
2895
+ for (const prop of bareChoiceProps) {
2896
+ if (containersWithUnion.has(prop.container)) {
2897
+ context.report({
2898
+ node: prop.node,
2899
+ messageId: "bareChoiceField",
2900
+ data: { name: prop.name }
2901
+ });
2902
+ }
2903
+ }
2904
+ }
2905
+ };
2906
+ function refKeyText(node) {
2907
+ if (node.type === import_utils26.AST_NODE_TYPES.BinaryExpression) {
2908
+ return refKey(node.left) ?? refKey(node.right) ?? "value";
2909
+ }
2910
+ if (node.type === import_utils26.AST_NODE_TYPES.SwitchStatement) {
2911
+ return refKey(node.discriminant) ?? "value";
2912
+ }
2913
+ return "value";
2914
+ }
2915
+ }
2916
+ });
2917
+
2918
+ // src/rules/single-public-export.ts
2919
+ var import_utils27 = require("@typescript-eslint/utils");
2920
+ var JUNK_DRAWER_STEMS = /* @__PURE__ */ new Set([
2921
+ "util",
2922
+ "utils",
2923
+ "helper",
2924
+ "helpers",
2925
+ "common",
2926
+ "constant",
2927
+ "constants",
2928
+ "type",
2929
+ "types",
2930
+ "model",
2931
+ "models",
2932
+ "shared",
2933
+ "misc"
2934
+ ]);
2935
+ var CONVENTIONAL_BUCKET_EXPORTS = /* @__PURE__ */ new Set(["cn"]);
2936
+ var ACRONYM_OVERRIDES = [
2937
+ [/OAuth/g, "Oauth"],
2938
+ [/GraphQL/g, "Graphql"],
2939
+ [/gRPC/g, "Grpc"]
2940
+ ];
2941
+ var CAMEL_BOUNDARY_RE = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
2942
+ var TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
2943
+ var SCRIPT_EXT_RE = /\.[cm]?[jt]sx?$/i;
2944
+ var basename = (filename) => filename.split(/[/\\]/).pop() ?? filename;
2945
+ var stemOf = (base) => base.replace(SCRIPT_EXT_RE, "");
2946
+ var kebabCase = (name) => {
2947
+ let normalized = name;
2948
+ for (const [pattern, replacement] of ACRONYM_OVERRIDES) {
2949
+ normalized = normalized.replace(pattern, replacement);
2950
+ }
2951
+ return normalized.replace(CAMEL_BOUNDARY_RE, "-").toLowerCase();
2952
+ };
2953
+ var isFunctionExpression2 = (node) => node !== null && (node.type === import_utils27.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils27.AST_NODE_TYPES.FunctionExpression);
2954
+ var functionConstName = (decl) => {
2955
+ if (decl.declarations.length !== 1) return null;
2956
+ const [declarator] = decl.declarations;
2957
+ if (declarator === void 0) return null;
2958
+ if (declarator.id.type !== import_utils27.AST_NODE_TYPES.Identifier) return null;
2959
+ if (!isFunctionExpression2(declarator.init)) return null;
2960
+ return declarator.id.name;
2961
+ };
2962
+ var summarizeExports = (body) => {
2963
+ let names = 0;
2964
+ let hasReExport = false;
2965
+ let candidate = null;
2966
+ const addCandidate = (name, node) => {
2967
+ names += 1;
2968
+ candidate = { name, node };
2969
+ };
2970
+ for (const statement of body) {
2971
+ switch (statement.type) {
2972
+ case import_utils27.AST_NODE_TYPES.ExportAllDeclaration:
2973
+ hasReExport = true;
2974
+ break;
2975
+ case import_utils27.AST_NODE_TYPES.ExportDefaultDeclaration: {
2976
+ names += 1;
2977
+ const decl = statement.declaration;
2978
+ if (decl.type === import_utils27.AST_NODE_TYPES.FunctionDeclaration && decl.id !== null) {
2979
+ candidate = { name: decl.id.name, node: statement };
2980
+ } else if (decl.type === import_utils27.AST_NODE_TYPES.ClassDeclaration && decl.id !== null) {
2981
+ candidate = { name: decl.id.name, node: statement };
2982
+ }
2983
+ break;
2984
+ }
2985
+ case import_utils27.AST_NODE_TYPES.ExportNamedDeclaration: {
2986
+ if (statement.source !== null) {
2987
+ hasReExport = true;
2988
+ break;
2989
+ }
2990
+ const decl = statement.declaration;
2991
+ if (decl === null) {
2992
+ names += statement.specifiers.length;
2993
+ break;
2994
+ }
2995
+ switch (decl.type) {
2996
+ case import_utils27.AST_NODE_TYPES.FunctionDeclaration:
2997
+ if (decl.id !== null) addCandidate(decl.id.name, statement);
2998
+ else names += 1;
2999
+ break;
3000
+ case import_utils27.AST_NODE_TYPES.ClassDeclaration:
3001
+ if (decl.id !== null) addCandidate(decl.id.name, statement);
3002
+ else names += 1;
3003
+ break;
3004
+ case import_utils27.AST_NODE_TYPES.VariableDeclaration: {
3005
+ const fnName = functionConstName(decl);
3006
+ if (fnName !== null && decl.declarations.length === 1) {
3007
+ addCandidate(fnName, statement);
3008
+ } else {
3009
+ names += decl.declarations.length;
3010
+ }
3011
+ break;
3012
+ }
3013
+ default:
3014
+ names += 1;
3015
+ }
3016
+ break;
3017
+ }
3018
+ default:
3019
+ break;
3020
+ }
3021
+ }
3022
+ return { names, hasReExport, candidate };
3023
+ };
3024
+ var single_public_export_default = import_utils27.ESLintUtils.RuleCreator(
3025
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
3026
+ )({
3027
+ name: "single-public-export",
3028
+ meta: {
3029
+ type: "suggestion",
3030
+ docs: {
3031
+ description: "A junk-drawer module stem (`utils`, `helpers`, `types`, ...) with a single public function/class/const export should be renamed after that export."
3032
+ },
3033
+ schema: [],
3034
+ messages: {
3035
+ renameJunkDrawer: "Module stem `{{stem}}` is a generic junk-drawer name; its sole public export is `{{name}}` \u2014 rename the file to `{{expected}}.ts` to describe its responsibility."
3036
+ }
3037
+ },
3038
+ defaultOptions: [],
3039
+ create(context) {
3040
+ const base = basename(context.filename);
3041
+ if (base.endsWith(".d.ts")) return {};
3042
+ if (TEST_FILE_RE.test(base)) return {};
3043
+ const stem = stemOf(base);
3044
+ if (!JUNK_DRAWER_STEMS.has(stem.toLowerCase())) return {};
3045
+ return {
3046
+ Program(node) {
3047
+ const { names, hasReExport, candidate } = summarizeExports(node.body);
3048
+ if (hasReExport) return;
3049
+ if (names !== 1 || candidate === null) return;
3050
+ if (CONVENTIONAL_BUCKET_EXPORTS.has(candidate.name)) return;
3051
+ const expected = kebabCase(candidate.name);
3052
+ if (stem === expected) return;
3053
+ context.report({
3054
+ node: candidate.node,
3055
+ messageId: "renameJunkDrawer",
3056
+ data: { stem, name: candidate.name, expected }
3057
+ });
3058
+ }
3059
+ };
3060
+ }
3061
+ });
3062
+
1939
3063
  // src/index.ts
1940
3064
  var rules = {
1941
3065
  "enforce-file-structure": enforce_file_structure_default,
@@ -1957,12 +3081,18 @@ var rules = {
1957
3081
  "prefer-shadcn": prefer_shadcn_default,
1958
3082
  "require-assert-never": require_assert_never_default,
1959
3083
  "require-zod-form-validation": require_zod_form_validation_default,
1960
- "zod-naming-convention": zod_naming_convention_default
3084
+ "zod-naming-convention": zod_naming_convention_default,
3085
+ "no-cors-wildcard-with-credentials": no_cors_wildcard_with_credentials_default,
3086
+ "no-fat-try-blocks": no_fat_try_blocks_default,
3087
+ "no-secret-in-log": no_secret_in_log_default,
3088
+ "no-template-literal-in-log": no_template_literal_in_log_default,
3089
+ "prefer-string-literal-union": prefer_string_literal_union_default,
3090
+ "single-public-export": single_public_export_default
1961
3091
  };
1962
3092
  var plugin = {
1963
3093
  meta: {
1964
3094
  name: "@sarj/eslint-plugin",
1965
- version: "2.2.0"
3095
+ version: "2.3.0"
1966
3096
  },
1967
3097
  rules,
1968
3098
  configs: {
@@ -1987,7 +3117,14 @@ var plugin = {
1987
3117
  "@sarj/prefer-discriminated-union": "warn",
1988
3118
  "@sarj/no-comment-cruft": "warn",
1989
3119
  // Frontend / styling — distilled from frontend PR-review mining.
1990
- "@sarj/prefer-semantic-colors": "warn"
3120
+ "@sarj/prefer-semantic-colors": "warn",
3121
+ // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
3122
+ "@sarj/no-fat-try-blocks": "warn",
3123
+ "@sarj/no-cors-wildcard-with-credentials": "warn",
3124
+ "@sarj/no-template-literal-in-log": "warn",
3125
+ "@sarj/no-secret-in-log": "warn",
3126
+ "@sarj/single-public-export": "warn",
3127
+ "@sarj/prefer-string-literal-union": "warn"
1991
3128
  }
1992
3129
  },
1993
3130
  strict: {
@@ -2015,7 +3152,15 @@ var plugin = {
2015
3152
  "@sarj/no-comment-cruft": "error",
2016
3153
  // Frontend / styling — distilled from frontend PR-review mining. Stylistic,
2017
3154
  // no autofix → warn (rollout should prove the FP rate before raising it).
2018
- "@sarj/prefer-semantic-colors": "warn"
3155
+ "@sarj/prefer-semantic-colors": "warn",
3156
+ // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
3157
+ "@sarj/no-fat-try-blocks": "error",
3158
+ "@sarj/no-cors-wildcard-with-credentials": "error",
3159
+ "@sarj/no-template-literal-in-log": "error",
3160
+ "@sarj/no-secret-in-log": "error",
3161
+ "@sarj/single-public-export": "error",
3162
+ // High-volume/stylistic — warn until rollout proves FP rate.
3163
+ "@sarj/prefer-string-literal-union": "warn"
2019
3164
  }
2020
3165
  }
2021
3166
  }