@sarj/eslint-plugin 2.2.0 → 2.3.1

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