@cosmicdrift/kumiko-guards 0.356.0 → 0.357.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.
Files changed (39) hide show
  1. package/package.json +2 -2
  2. package/src/_lib/bun-lock-packages.ts +5 -5
  3. package/src/_lib/guard-kit.ts +180 -131
  4. package/src/_lib/qn.ts +9 -9
  5. package/src/_lib/roots.ts +4 -3
  6. package/src/_lib/security-baseline.ts +193 -116
  7. package/src/_lib/sql-inventory.ts +34 -32
  8. package/src/changes.json +6 -0
  9. package/src/check-as-casts.ts +196 -180
  10. package/src/check-complexity.ts +68 -69
  11. package/src/check-predicates.ts +93 -67
  12. package/src/check-secret-literals.ts +49 -38
  13. package/src/check-single-runtime-instance.ts +1 -1
  14. package/src/cli.ts +30 -17
  15. package/src/guard-access-denied-test.ts +49 -23
  16. package/src/guard-app-feature-structure.ts +92 -73
  17. package/src/guard-broker-subscribe.ts +17 -17
  18. package/src/guard-comment-lang.ts +126 -54
  19. package/src/guard-direct-entity-writes.ts +58 -45
  20. package/src/guard-escape-hatch-declared.ts +26 -23
  21. package/src/guard-html-escape.ts +112 -86
  22. package/src/guard-i18n-keys.ts +39 -36
  23. package/src/guard-i18n-locale-mount.ts +47 -40
  24. package/src/guard-i18n-ui-strings.ts +108 -92
  25. package/src/guard-lib-test-coverage.ts +37 -37
  26. package/src/guard-loadall-events.ts +17 -17
  27. package/src/guard-no-framed-extension-sections.ts +4 -4
  28. package/src/guard-no-temporal-polyfill-import.ts +19 -1
  29. package/src/guard-pii-annotations.ts +16 -16
  30. package/src/guard-primitives-discipline.ts +61 -65
  31. package/src/guard-screen-conventions.ts +2 -0
  32. package/src/guard-table-ddl.ts +56 -62
  33. package/src/guard-tenant-escalation.ts +42 -34
  34. package/src/guard-test-stack-drift.ts +6 -6
  35. package/src/guard-test-timeouts.ts +5 -4
  36. package/src/guard-text-field-stance.ts +22 -22
  37. package/src/guard-thin-wrappers.ts +42 -45
  38. package/src/guard-write-handler-qns.ts +29 -29
  39. package/src/runtime-isolation-classify.ts +4 -3
@@ -32,6 +32,7 @@ import {
32
32
  type ArrowFunction,
33
33
  type FunctionDeclaration,
34
34
  type FunctionExpression,
35
+ type Node,
35
36
  Project,
36
37
  type SourceFile,
37
38
  SyntaxKind,
@@ -230,6 +231,38 @@ function getFnName(node: FnNode): string | undefined {
230
231
  return undefined;
231
232
  }
232
233
 
234
+ function collectCalleeNames(node: FnNode): Set<string> {
235
+ const callees = new Set<string>();
236
+ node.forEachDescendant((n) => {
237
+ if (n.isKind(SyntaxKind.CallExpression)) {
238
+ const name = extractCalleeName(n);
239
+ if (name) callees.add(name);
240
+ }
241
+ });
242
+ return callees;
243
+ }
244
+
245
+ function wrapperCalleeName(callExpr: Node): string | null {
246
+ const name = extractCalleeName(callExpr);
247
+ if (!name) return null;
248
+ if (BUILTIN_NAMES.has(name)) return null;
249
+ return name;
250
+ }
251
+
252
+ // The single call expression a one-statement body consists of, if it has that shape.
253
+ function singleStatementCall(single: Node): Node | null {
254
+ if (single.isKind(SyntaxKind.ReturnStatement)) {
255
+ const expr = single.getExpression();
256
+ // `return () => ...` is a factory, not a delegation
257
+ return expr?.isKind(SyntaxKind.CallExpression) ? expr : null;
258
+ }
259
+ if (single.isKind(SyntaxKind.ExpressionStatement)) {
260
+ const expr = single.getExpression();
261
+ return expr.isKind(SyntaxKind.CallExpression) ? expr : null;
262
+ }
263
+ return null;
264
+ }
265
+
233
266
  function getSingleDirectCallee(node: FnNode): string | null {
234
267
  const rawBody =
235
268
  node.isKind(SyntaxKind.ArrowFunction) && !node.getBody().isKind(SyntaxKind.Block)
@@ -241,59 +274,21 @@ function getSingleDirectCallee(node: FnNode): string | null {
241
274
  const body = node.getBody();
242
275
  if (!body?.isKind(SyntaxKind.CallExpression)) return null;
243
276
  // Method chains (A.from(x).toMethod(y)) have 2 call expressions — not a thin wrapper
244
- const conciseCallees = new Set<string>();
245
- node.forEachDescendant((n) => {
246
- if (!n.isKind(SyntaxKind.CallExpression)) return;
247
- const cName = extractCalleeName(n);
248
- if (cName) conciseCallees.add(cName);
249
- });
250
- if (conciseCallees.size !== 1) return null;
251
- const name = extractCalleeName(body);
252
- if (!name || BUILTIN_NAMES.has(name)) return null;
253
- return name;
277
+ if (collectCalleeNames(node).size !== 1) return null;
278
+ return wrapperCalleeName(body);
254
279
  }
255
280
 
256
281
  const stmts = rawBody.getStatements();
257
282
  if (stmts.length !== 1) return null;
258
283
 
259
- const single = stmts.reduce((only) => only);
260
- // switch → not a wrapper
261
- if (single.isKind(SyntaxKind.SwitchStatement)) return null;
262
-
263
- let callExpr = null;
264
-
265
- if (single.isKind(SyntaxKind.ReturnStatement)) {
266
- const expr = single.getExpression();
267
- if (!expr) return null;
268
- // factory: return () => ... → not a wrapper
269
- if (expr.isKind(SyntaxKind.ArrowFunction) || expr.isKind(SyntaxKind.FunctionExpression))
270
- return null;
271
- // direct delegation: return someCall(...)
272
- if (!expr.isKind(SyntaxKind.CallExpression)) return null;
273
- callExpr = expr;
274
- } else if (single.isKind(SyntaxKind.ExpressionStatement)) {
275
- const expr = single.getExpression();
276
- if (!expr.isKind(SyntaxKind.CallExpression)) return null;
277
- callExpr = expr;
278
- } else {
279
- return null;
280
- }
284
+ const callExpr = singleStatementCall(stmts.reduce((only) => only));
285
+ if (!callExpr) return null;
281
286
 
282
287
  // Ensure the call isn't part of a larger nested-call tree: a direct call
283
288
  // has exactly 1 unique callee name across the whole body.
284
- const allCallees = new Set<string>();
285
- node.forEachDescendant((n) => {
286
- if (!n.isKind(SyntaxKind.CallExpression)) return;
287
- const name = extractCalleeName(n);
288
- if (name) allCallees.add(name);
289
- });
290
- if (allCallees.size !== 1) return null;
291
-
292
- const name = extractCalleeName(callExpr);
293
- if (!name) return null;
294
- if (BUILTIN_NAMES.has(name)) return null;
289
+ if (collectCalleeNames(node).size !== 1) return null;
295
290
 
296
- return name;
291
+ return wrapperCalleeName(callExpr);
297
292
  }
298
293
 
299
294
  function extractCalleeName(callNode: ReturnType<FnNode["getBody"]> | undefined): string | null {
@@ -349,11 +344,13 @@ export function collectFindings(sf: SourceFile): Finding[] {
349
344
 
350
345
  const check = (node: FnNode) => {
351
346
  const fnName = getFnName(node);
347
+ // skip: anonymous functions cannot be reported by name
352
348
  if (!fnName) return;
353
349
 
354
350
  const callee = getSingleDirectCallee(node);
351
+ // skip: not a single-callee wrapper
355
352
  if (!callee) return;
356
- // Self-recursion / delegation false-positive (same name)
353
+ // skip: self-recursion / same-name delegation is a false positive
357
354
  if (callee === fnName) return;
358
355
 
359
356
  const markerReason = extractMarkerReason(node);
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Guard: prüft dass `dispatcher.write("qn:literal:...")`-Aufrufe in
4
- * Custom-Screens gültige Write-Handler-QNs referenzieren. Tippfehler
5
- * im QN fallen sonst erst zur Runtime als 404 auf.
3
+ * Guard: checks that `dispatcher.write("qn:literal:...")` calls in
4
+ * custom screens reference valid write-handler QNs. Typos
5
+ * in the QN would otherwise only surface at runtime as a 404.
6
6
  *
7
- * Validierung in zwei Stufen:
8
- * 1. **Strukturell**: jeder QN muss `:write:` enthalten — fängt
9
- * offensichtliche Tippfehler ("feautre:write:create").
10
- * 2. **Gegen Manifest**: wenn `feature-manifest.json` im Repo-Root
11
- * liegt und `writeHandlers` enthält, matched der Guard dagegen.
7
+ * Validation in two stages:
8
+ * 1. **Structural**: every QN must contain `:write:` — catches
9
+ * obvious typos ("feautre:write:create").
10
+ * 2. **Against manifest**: if `feature-manifest.json` is in the repo root
11
+ * and contains `writeHandlers`, the guard matches against it.
12
12
  *
13
13
  * Usage:
14
14
  * bun infra/guards/guard-write-handler-qns.ts
@@ -94,11 +94,11 @@ function findKnownQns(filePath: string, entries: ReadonlyArray<ManifestEntry>):
94
94
  }
95
95
 
96
96
  /**
97
- * Extrahiert den Literal-Wert aus einem String-Literal, einem
98
- * Backtick-Literal ohne Interpolation (`NoSubstitutionTemplateLiteral`) oder
99
- * einem `<literal> as T`-Cast. Backtick-Konstanten (`const QN = \`x:write:y\``)
100
- * wurden vorher nur als direktes Argument, nicht als aufgelöste Deklaration
101
- * erkannt.
97
+ * Extracts the literal value from a string literal, a backtick literal
98
+ * without interpolation (`NoSubstitutionTemplateLiteral`) or a
99
+ * `<literal> as T` cast. Backtick constants (`const QN = \`x:write:y\``)
100
+ * used to be recognized only as a direct argument, not as a resolved
101
+ * declaration.
102
102
  */
103
103
  function literalValueOf(node: Node): string | undefined {
104
104
  if (
@@ -137,8 +137,8 @@ export function resolveWriteQnFromArg(node: Node): string | undefined {
137
137
  }
138
138
 
139
139
  /**
140
- * Scannt eine SourceFile nach `dispatcher.write(<stringLiteral>, ...)`-
141
- * oder `<expr>.write(<stringLiteral>, ...)`-Aufrufen.
140
+ * Scans a SourceFile for `dispatcher.write(<stringLiteral>, ...)`
141
+ * or `<expr>.write(<stringLiteral>, ...)` calls.
142
142
  */
143
143
  export function scanDispatcherWriteCalls(
144
144
  sf: SourceFile,
@@ -148,14 +148,14 @@ export function scanDispatcherWriteCalls(
148
148
  const expr = call.getExpression();
149
149
  const exprText = expr.getText();
150
150
 
151
- // `dispatcher.write(...)` oder irgendein `<obj>.write(...)` (ein
152
- // aliaster Dispatcher heißt nicht zwingend "dispatcher").
151
+ // `dispatcher.write(...)` or any `<obj>.write(...)` (an aliased
152
+ // dispatcher is not necessarily named "dispatcher").
153
153
  const isDispatcherCall = exprText === "dispatcher.write";
154
154
  if (!isDispatcherCall && !exprText.endsWith(".write")) continue;
155
155
 
156
- // Erster Parameter muss ein String-Literal sein — dynamische
157
- // QNs (Handler-Konstanten, Template-Literale) werden nicht
158
- // validiert (sind entweder typ-safe oder nicht prüfbar).
156
+ // The first parameter must be a string literal — dynamic QNs (handler
157
+ // constants, template literals) are not validated (they are either
158
+ // type-safe or not checkable).
159
159
  const args = call.getArguments();
160
160
  const first = args[0];
161
161
  if (!first) continue;
@@ -163,11 +163,11 @@ export function scanDispatcherWriteCalls(
163
163
  const qn = resolveWriteQnFromArg(first);
164
164
  if (qn === undefined) continue;
165
165
 
166
- // `.endsWith(".write")` matcht auch Nicht-Dispatcher-Writes:
166
+ // `.endsWith(".write")` also matches non-dispatcher writes:
167
167
  // `res.write(...)`, `stream.write(...)`, SSE `res.write("data: …")`.
168
- // Deren Argument ist kein QN → würde sonst als "ungültiges QN-Format"
169
- // false-positiv gemeldet. Außerhalb eines expliziten `dispatcher.write`
170
- // nur prüfen, wenn das Literal ein Write-QN ist (enthält ":write:").
168
+ // Their argument is not a QN → it would otherwise be reported as an
169
+ // "invalid QN format" false positive. Outside an explicit `dispatcher.write`
170
+ // only check when the literal is a write QN (contains ":write:").
171
171
  if (!isDispatcherCall && !qn.includes(":write:")) continue;
172
172
 
173
173
  hits.push({
@@ -212,9 +212,9 @@ export const guard: AstGuard = {
212
212
  continue;
213
213
  }
214
214
 
215
- // Stufe 2: gegen Manifest matchen (wenn vorhanden).
216
- // QN wird vor dem Match auf kebab-case normalisiert, sodass
217
- // camelCase- und kebab-case-Eingaben gleich behandelt werden.
215
+ // Stage 2: match against the manifest (if present).
216
+ // The QN is normalized to kebab-case before the match, so that
217
+ // camelCase and kebab-case inputs are treated the same.
218
218
  const normalizedQn = toKebab(hit.qn);
219
219
  if (knownQns.size > 0 && !knownQns.has(normalizedQn)) {
220
220
  violations.push({
@@ -226,8 +226,8 @@ export const guard: AstGuard = {
226
226
  }
227
227
  }
228
228
 
229
- // Info-Log wenn kein Manifest geladen wurde — kein Fehler, aber
230
- // der Guard läuft dann nur mit struktureller Prüfung.
229
+ // Info log when no manifest was loaded — not an error, but
230
+ // the guard then runs with the structural check only.
231
231
  if (knownQnsByRepo.size === 0 && violations.length === 0) {
232
232
  console.warn(
233
233
  " [INFO] No feature-manifest.json with writeHandlers found — structural check only.",
@@ -421,9 +421,10 @@ export function findRuntimeIsolationViolations(
421
421
  const outsideRoot: string[] = [];
422
422
  const seenOutside = new Set<string>();
423
423
  const noteOutside = (fp: string) => {
424
- if (seenOutside.has(fp)) return;
425
- seenOutside.add(fp);
426
- outsideRoot.push(fp);
424
+ if (!seenOutside.has(fp)) {
425
+ seenOutside.add(fp);
426
+ outsideRoot.push(fp);
427
+ }
427
428
  };
428
429
  const stats: Record<Runtime, number> = {
429
430
  runtime: 0,