@starci/eslint-canon-fe 9.5.23 → 9.6.2

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 (164) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/button-intent.md +1 -1
  3. package/package.json +1 -1
  4. package/runtime/bundle.json +192 -0
  5. package/runtime/engine/by-code-unit.mjs +6 -6
  6. package/runtime/engine/model-config.mjs +98 -76
  7. package/runtime/engine/orca-config.mjs +22 -17
  8. package/runtime/engine/release-config.mjs +22 -18
  9. package/runtime/engine/removed-vocabulary.mjs +21 -20
  10. package/runtime/engine/resources-config.mjs +42 -35
  11. package/runtime/engine/runtime-root.mjs +11 -13
  12. package/runtime/engine/secrets.mjs +170 -102
  13. package/runtime/engine/sonar-config.mjs +16 -13
  14. package/runtime/engine/temp-root.mjs +3 -9
  15. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  16. package/runtime/knowledge/hfs/slots.yaml +2 -2
  17. package/runtime/knowledge/sonar-gate.yaml +1 -1
  18. package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -1
  19. package/runtime/scripts/api/git/cat-file.mjs +0 -1
  20. package/runtime/scripts/api/git/config-get.mjs +1 -2
  21. package/runtime/scripts/api/git/diff.mjs +0 -1
  22. package/runtime/scripts/api/git/lib.mjs +32 -32
  23. package/runtime/scripts/api/git/ls-files.mjs +0 -1
  24. package/runtime/scripts/api/git/ls-tree.mjs +0 -1
  25. package/runtime/scripts/api/git/merge-base.mjs +2 -3
  26. package/runtime/scripts/api/git/remote.mjs +0 -1
  27. package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
  28. package/runtime/scripts/api/git/show.mjs +0 -1
  29. package/runtime/scripts/hfs/allows.mjs +37 -38
  30. package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
  31. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
  32. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
  33. package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
  34. package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
  35. package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
  36. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
  37. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
  38. package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
  39. package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
  40. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  41. package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
  42. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
  43. package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
  44. package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
  45. package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
  46. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
  47. package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
  48. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
  49. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
  50. package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
  51. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
  52. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
  53. package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
  54. package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
  55. package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
  56. package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
  57. package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
  58. package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
  59. package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
  60. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
  61. package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
  62. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
  63. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
  64. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
  65. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
  66. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
  67. package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
  68. package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
  69. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
  70. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
  71. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
  72. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
  73. package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
  74. package/runtime/scripts/hfs/architecture/index.mjs +272 -272
  75. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
  76. package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
  77. package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
  78. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
  79. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
  80. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
  81. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
  82. package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
  83. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
  84. package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
  85. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
  86. package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
  87. package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
  88. package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
  89. package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
  90. package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
  91. package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
  92. package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
  93. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
  94. package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
  95. package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
  96. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
  99. package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
  100. package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
  101. package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
  102. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
  103. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
  104. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
  105. package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
  113. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  114. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  115. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  116. package/runtime/scripts/hfs/linear-text.mjs +20 -20
  117. package/runtime/scripts/hfs/param-names.mjs +6 -11
  118. package/runtime/scripts/hfs/path-findings.mjs +120 -100
  119. package/runtime/scripts/hfs/repo-identity.mjs +55 -42
  120. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  121. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  122. package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
  123. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
  124. package/runtime/scripts/hfs/rules/read.mjs +19 -15
  125. package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
  126. package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
  127. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  128. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  129. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  130. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  131. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  132. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  133. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  134. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  135. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  136. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  137. package/runtime/scripts/hfs/slots.mjs +175 -152
  138. package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
  139. package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
  140. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  141. package/runtime/scripts/hfs/view.mjs +38 -40
  142. package/runtime/scripts/lib/dockerfile.mjs +131 -114
  143. package/runtime/scripts/lib/env.mjs +3 -6
  144. package/runtime/scripts/lib/event-contract.mjs +73 -78
  145. package/runtime/scripts/lib/git.mjs +15 -16
  146. package/runtime/scripts/lib/glob.mjs +23 -14
  147. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  148. package/runtime/scripts/lib/i18n.mjs +32 -35
  149. package/runtime/scripts/lib/in-order.mjs +57 -38
  150. package/runtime/scripts/lib/json-clone.mjs +45 -36
  151. package/runtime/scripts/lib/json.mjs +26 -14
  152. package/runtime/scripts/lib/language.mjs +133 -129
  153. package/runtime/scripts/lib/list.mjs +9 -14
  154. package/runtime/scripts/lib/mutation-fence.mjs +10 -5
  155. package/runtime/scripts/lib/path-key.mjs +53 -38
  156. package/runtime/scripts/lib/read-text.mjs +30 -17
  157. package/runtime/scripts/lib/regex.mjs +1 -2
  158. package/runtime/scripts/lib/same-text.mjs +0 -1
  159. package/runtime/scripts/lib/sops-envelope.mjs +148 -120
  160. package/runtime/scripts/lib/stack-services.mjs +128 -129
  161. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  162. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  163. package/runtime/scripts/lib/walk.mjs +33 -27
  164. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { machineKit } from './machine-ast.mjs';
3
3
  import { constructorDependencies, serviceClasses } from './constructor-deps.mjs';
4
-
4
+ const hasInjector = (dependency) => 'decoratorDeclaration' in dependency && 'injectorCall' in dependency
5
+ && Boolean(dependency.decoratorDeclaration) && Boolean(dependency.injectorCall);
5
6
  /**
6
7
  * R85 `injection-token-exported` (BE_RAW_INJECT), the injection law's last obligation: a spec can only provide a token it
7
8
  * can import. Every `Inject<Thing>()` decorator that a service constructor uses resolves to a token (the argument of its
@@ -12,56 +13,57 @@ import { constructorDependencies, serviceClasses } from './constructor-deps.mjs'
12
13
  * The finding sits on the decorator's `injector(...)` call, once per decorator, however many services use it.
13
14
  */
14
15
  export const INJECTION_TOKEN_EXPORTED_RULE_IDS = ['BE_RAW_INJECT'];
15
-
16
16
  const RULE = 'BE_RAW_INJECT';
17
-
18
17
  const exportedVariableHasName = (ts, kit, statement, name) => {
19
- if (!ts.isVariableStatement(statement) || !kit.isExported(statement)) return false;
20
- return statement.declarationList.declarations.some(declaration =>
21
- (ts.isIdentifier(declaration.name) && declaration.name.text === name)
22
- || (ts.isObjectBindingPattern(declaration.name) && declaration.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === name)));
18
+ // The modifier flags are read off the statement itself; the kit types its argument as a declaration.
19
+ if (!ts.isVariableStatement(statement) || !kit.isExported(statement))
20
+ return false;
21
+ return statement.declarationList.declarations.some(declaration => (ts.isIdentifier(declaration.name) && declaration.name.text === name)
22
+ || (ts.isObjectBindingPattern(declaration.name) && declaration.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === name)));
23
23
  };
24
-
25
24
  const exportedDeclarationHasName = (ts, statement, name) => {
26
- if (!ts.isExportDeclaration(statement)) return false;
27
- const clause = statement.exportClause;
28
- return clause !== undefined && ts.isNamedExports(clause) && clause.elements.some(element => element.name.text === name);
25
+ if (!ts.isExportDeclaration(statement))
26
+ return false;
27
+ const clause = statement.exportClause;
28
+ return clause !== undefined && ts.isNamedExports(clause) && clause.elements.some(element => element.name.text === name);
29
29
  };
30
-
31
- const exportsName = (ts, kit, file, name) => file?.sourceFile.statements.some(statement =>
32
- exportedVariableHasName(ts, kit, statement, name) || exportedDeclarationHasName(ts, statement, name)) === true;
33
-
30
+ const exportsName = (ts, kit, file, name) => file?.sourceFile.statements.some(statement => exportedVariableHasName(ts, kit, statement, name) || exportedDeclarationHasName(ts, statement, name)) === true;
34
31
  const indexOf = (graph, file) => (file?.owner ? graph.files.get(`${file.owner.root}/index.ts`) : null);
35
-
32
+ /** The graph file declaring `declaration`, when the program holds it. */
33
+ const graphFileOf = (graph, kit, declaration) => {
34
+ const rel = kit.graphPath(declaration);
35
+ return rel === null ? undefined : graph.files.get(rel);
36
+ };
36
37
  const injectionViolationFor = (graph, kit, ts, dependency) => {
37
- const decoratorFile = graph.files.get(kit.graphPath(dependency.decoratorDeclaration));
38
- if (dependency.kind === 'unresolved') {
39
- return decoratorFile ? { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator,
40
- message: `${dependency.decorator}() cannot be followed to its token (${dependency.reason}); name an exported \`unique symbol\` const so a unit spec can provide it.` } : null;
41
- }
42
- const tokenFile = dependency.declaration ? graph.files.get(kit.graphPath(dependency.declaration)) : null;
43
- const homes = [decoratorFile, tokenFile && path.posix.basename(tokenFile.rel).endsWith('.decorators.ts') ? tokenFile : null, indexOf(graph, decoratorFile), indexOf(graph, tokenFile)];
44
- if (!decoratorFile || homes.some(home => exportsName(ts, kit, home, dependency.name))) return null;
45
- return { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator, token: dependency.name,
46
- message: `${dependency.decorator}() injects ${dependency.name}, which is not exported from ${path.posix.basename(decoratorFile.rel)} nor from the capability index.ts; export the token so a unit spec can provide it (\`{ provide: ${dependency.name}, useValue: double }\`).` };
38
+ const decoratorFile = graphFileOf(graph, kit, dependency.decoratorDeclaration);
39
+ if (dependency.kind === 'unresolved') {
40
+ return decoratorFile ? { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator,
41
+ message: `${dependency.decorator}() cannot be followed to its token (${dependency.reason}); name an exported \`unique symbol\` const so a unit spec can provide it.` } : null;
42
+ }
43
+ const tokenFile = dependency.declaration ? graphFileOf(graph, kit, dependency.declaration) : null;
44
+ const homes = [decoratorFile, tokenFile && path.posix.basename(tokenFile.rel).endsWith('.decorators.ts') ? tokenFile : null, indexOf(graph, decoratorFile), indexOf(graph, tokenFile)];
45
+ if (!decoratorFile || homes.some(home => exportsName(ts, kit, home, dependency.name)))
46
+ return null;
47
+ return { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator, token: dependency.name,
48
+ message: `${dependency.decorator}() injects ${dependency.name}, which is not exported from ${path.posix.basename(decoratorFile.rel)} nor from the capability index.ts; export the token so a unit spec can provide it (\`{ provide: ${dependency.name}, useValue: double }\`).` };
47
49
  };
48
-
49
50
  export function checkInjectionTokenExported(input) {
50
- const { graph } = input;
51
- const kit = machineKit(input);
52
- const { ts } = kit;
53
- const violations = [];
54
- const seen = new Set();
55
- let decorators = 0;
56
-
57
- for (const { file, declaration } of serviceClasses(kit, graph)) {
58
- for (const dependency of constructorDependencies(kit, file, declaration)) {
59
- if (dependency.external || !dependency.decoratorDeclaration || !dependency.injectorCall || seen.has(dependency.injectorCall)) continue;
60
- seen.add(dependency.injectorCall);
61
- decorators += 1;
62
- const violation = injectionViolationFor(graph, kit, ts, dependency);
63
- if (violation) violations.push(violation);
51
+ const { graph } = input;
52
+ const kit = machineKit(input);
53
+ const { ts } = kit;
54
+ const violations = [];
55
+ const seen = new Set();
56
+ let decorators = 0;
57
+ for (const { file, declaration } of serviceClasses(kit, graph)) {
58
+ for (const dependency of constructorDependencies(kit, file, declaration)) {
59
+ if (!hasInjector(dependency) || (dependency.kind === 'token' && dependency.external) || seen.has(dependency.injectorCall))
60
+ continue;
61
+ seen.add(dependency.injectorCall);
62
+ decorators += 1;
63
+ const violation = injectionViolationFor(graph, kit, ts, dependency);
64
+ if (violation)
65
+ violations.push(violation);
66
+ }
64
67
  }
65
- }
66
- return { violations, coverage: { status: 'checked', decorators, files: graph.files.size } };
68
+ return { violations, coverage: { status: 'checked', decorators, files: graph.files.size } };
67
69
  }
@@ -3,210 +3,229 @@
3
3
  import { unwrapExpression } from '../../lib/ts-analysis.mjs';
4
4
  import { accessName as memberName, hasAnyFlag } from '../../lib/ts-ast.mjs';
5
5
  export { accessName as memberName } from '../../lib/ts-ast.mjs';
6
-
7
6
  export const moduleNameOf = (ts, node) => {
8
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) return node.moduleSpecifier.text;
9
- if (!ts.isCallExpression(node)) return null;
10
- const dynamic = node.expression.kind === ts.SyntaxKind.ImportKeyword;
11
- const required = ts.isIdentifier(node.expression) && node.expression.text === 'require';
12
- return (dynamic || required) && node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null;
7
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier))
8
+ return node.moduleSpecifier.text;
9
+ if (!ts.isCallExpression(node))
10
+ return null;
11
+ const dynamic = node.expression.kind === ts.SyntaxKind.ImportKeyword;
12
+ const required = ts.isIdentifier(node.expression) && node.expression.text === 'require';
13
+ const first = node.arguments[0];
14
+ return (dynamic || required) && node.arguments.length === 1 && first && ts.isStringLiteralLike(first) ? first.text : null;
13
15
  };
14
-
15
16
  export const importedFrom = (kit, checker, node, modules, names = null) => {
16
- const binding = kit.importBinding(checker, node);
17
- return Boolean(binding && modules.has(binding.module) && (names === null || names.has(binding.name)));
17
+ const binding = kit.importBinding(checker, node);
18
+ return Boolean(binding && modules.has(binding.module) && (names === null || names.has(binding.name)));
18
19
  };
19
-
20
- export const unwrap = (ts, node) => unwrapExpression(ts, node, { awaitExpressions: true, satisfiesExpressions: false });
21
-
20
+ export function unwrap(ts, node) {
21
+ return unwrapExpression(ts, node, { awaitExpressions: true, satisfiesExpressions: false });
22
+ }
22
23
  export const callName = (ts, call) => ts.isCallExpression(call) ? memberName(ts, call.expression) : null;
23
-
24
24
  export const chainParts = (ts, expression) => {
25
- const names = [];
26
- let current = unwrap(ts, expression);
27
- while (current) {
28
- if (ts.isCallExpression(current)) {
29
- current = unwrap(ts, current.expression);
30
- continue;
25
+ const names = [];
26
+ let current = unwrap(ts, expression);
27
+ while (current) {
28
+ if (ts.isCallExpression(current)) {
29
+ current = unwrap(ts, current.expression);
30
+ continue;
31
+ }
32
+ if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
33
+ const name = memberName(ts, current);
34
+ if (name)
35
+ names.unshift(name);
36
+ current = unwrap(ts, current.expression);
37
+ continue;
38
+ }
39
+ break;
31
40
  }
32
- if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
33
- const name = memberName(ts, current);
34
- if (name) names.unshift(name);
35
- current = unwrap(ts, current.expression);
36
- continue;
37
- }
38
- break;
39
- }
40
- return names;
41
+ return names;
41
42
  };
42
-
43
43
  export const reportAt = (violations, kit, ruleId, file, node, message, extra = {}) => {
44
- violations.push({ ruleId, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
44
+ violations.push({ ruleId, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
45
45
  };
46
-
47
46
  const declarationsOf = (ts, checker, node) => {
48
- const target = ts.isPropertyAccessExpression(node) ? node.name : node;
49
- // A shorthand property (`issuer,`) names the variable it reads, not the property it creates.
50
- let symbol = ts.isShorthandPropertyAssignment(target.parent ?? {}) && target.parent.name === target
51
- ? checker.getShorthandAssignmentValueSymbol(target.parent)
52
- : checker.getSymbolAtLocation(target);
53
- if (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
54
- return symbol?.declarations ?? [];
55
- };
56
-
47
+ const target = ts.isPropertyAccessExpression(node) ? node.name : node;
48
+ const parent = target.parent;
49
+ // A shorthand property (`issuer,`) names the variable it reads, not the property it creates.
50
+ let symbol = parent && ts.isShorthandPropertyAssignment(parent) && parent.name === target
51
+ ? checker.getShorthandAssignmentValueSymbol(parent)
52
+ : checker.getSymbolAtLocation(target);
53
+ if (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias))
54
+ symbol = checker.getAliasedSymbol(symbol);
55
+ return symbol?.declarations ?? [];
56
+ };
57
57
  const configType = (checker, node) => {
58
- const type = checker.getTypeAtLocation(node);
59
- return [type.symbol, type.aliasSymbol].some(symbol => symbol?.declarations?.some(declaration =>
60
- /(?:\.config|\.options|\.env)\.ts$|\/modules\/config\/|\/node_modules\/@nestjs\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'))
61
- || /(?:Config|Options|Env)$/u.test(symbol.name)));
58
+ const type = checker.getTypeAtLocation(node);
59
+ return [type.symbol, type.aliasSymbol].some(symbol => symbol?.declarations?.some(declaration => /(?:\.config|\.options|\.env)\.ts$|\/modules\/config\/|\/node_modules\/@nestjs\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'))
60
+ || /(?:Config|Options|Env)$/u.test(symbol.name)));
62
61
  };
63
-
64
62
  /** Trace an issuer or audience to typed configuration values through local URL/string transformations. */
65
63
  export const fromConfig = (ts, checker, expression, seen = new Set()) => {
66
- const node = unwrap(ts, expression);
67
- if (!node || seen.has(node)) return false;
68
- const next = new Set(seen).add(node);
69
- if (ts.isIdentifier(node)) {
70
- return declarationsOf(ts, checker, node).some(declaration => {
71
- if (ts.isVariableDeclaration(declaration) && declaration.initializer) return fromConfig(ts, checker, declaration.initializer, next);
72
- if (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) return configType(checker, node);
73
- return /(?:\.config|\.env)\.ts$|\/modules\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'));
74
- });
75
- }
76
- if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
77
- return fromConfig(ts, checker, node.expression, next) || declarationsOf(ts, checker, node).some(declaration =>
78
- (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) && !declaration.initializer && configType(checker, node));
79
- }
80
- if (ts.isTemplateExpression(node)) return node.templateSpans.some(span => fromConfig(ts, checker, span.expression, next));
81
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
82
- return fromConfig(ts, checker, node.left, next) || fromConfig(ts, checker, node.right, next);
83
- }
84
- if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
85
- return fromConfig(ts, checker, node.expression, next) || node.arguments?.some(argument => fromConfig(ts, checker, argument, next));
86
- }
87
- return false;
88
- };
89
-
64
+ const node = unwrap(ts, expression);
65
+ if (!node || seen.has(node))
66
+ return false;
67
+ const next = new Set(seen).add(node);
68
+ if (ts.isIdentifier(node)) {
69
+ return declarationsOf(ts, checker, node).some(declaration => {
70
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer)
71
+ return fromConfig(ts, checker, declaration.initializer, next);
72
+ if (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration))
73
+ return configType(checker, node);
74
+ return /(?:\.config|\.env)\.ts$|\/modules\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'));
75
+ });
76
+ }
77
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
78
+ return fromConfig(ts, checker, node.expression, next) || declarationsOf(ts, checker, node).some(declaration => (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) && !declaration.initializer && configType(checker, node));
79
+ }
80
+ if (ts.isTemplateExpression(node))
81
+ return node.templateSpans.some(span => fromConfig(ts, checker, span.expression, next));
82
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
83
+ return fromConfig(ts, checker, node.left, next) || fromConfig(ts, checker, node.right, next);
84
+ }
85
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
86
+ return fromConfig(ts, checker, node.expression, next) || node.arguments?.some(argument => fromConfig(ts, checker, argument, next));
87
+ }
88
+ return false;
89
+ };
90
90
  const objectValue = (ts, checker, expression) => {
91
- const node = unwrap(ts, expression);
92
- if (!node) return null;
93
- if (ts.isObjectLiteralExpression(node)) return node;
94
- if (!ts.isIdentifier(node)) return null;
95
- return declarationsOf(ts, checker, node).find(declaration => ts.isVariableDeclaration(declaration)
96
- && declaration.initializer && ts.isObjectLiteralExpression(unwrap(ts, declaration.initializer)))?.initializer ?? null;
97
- };
98
-
91
+ const node = unwrap(ts, expression);
92
+ if (!node)
93
+ return null;
94
+ if (ts.isObjectLiteralExpression(node))
95
+ return node;
96
+ if (!ts.isIdentifier(node))
97
+ return null;
98
+ const declared = declarationsOf(ts, checker, node).find((declaration) => ts.isVariableDeclaration(declaration)
99
+ && declaration.initializer !== undefined && ts.isObjectLiteralExpression(unwrap(ts, declaration.initializer)));
100
+ // The declaration is the one whose initializer unwraps to an object literal, so the unwrapped node is what is returned.
101
+ const initializer = unwrap(ts, declared?.initializer);
102
+ return initializer && ts.isObjectLiteralExpression(initializer) ? initializer : null;
103
+ };
99
104
  const literalString = (ts, checker, expression) => {
100
- const node = unwrap(ts, expression);
101
- if (!node) return null;
102
- if (ts.isStringLiteralLike(node)) return node.text;
103
- const type = checker.getTypeAtLocation(node);
104
- return type.isStringLiteral?.() ? type.value : null;
105
+ const node = unwrap(ts, expression);
106
+ if (!node)
107
+ return null;
108
+ if (ts.isStringLiteralLike(node))
109
+ return node.text;
110
+ const type = checker.getTypeAtLocation(node);
111
+ return type.isStringLiteral?.() ? type.value : null;
105
112
  };
106
-
107
113
  /** A role read must belong to the payload produced by this verification, including destructuring aliases. */
108
114
  const verifiedPayload = (ts, checker, expression, call, seen = new Set()) => {
109
- const node = unwrap(ts, expression);
110
- if (!node || seen.has(node)) return false;
111
- const next = new Set(seen).add(node);
112
- if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && memberName(ts, node) === 'payload') {
113
- const value = unwrap(ts, node.expression);
114
- if (value === call) return true;
115
- if (ts.isIdentifier(value)) return declarationsOf(ts, checker, value).some(declaration =>
116
- ts.isVariableDeclaration(declaration) && unwrap(ts, declaration.initializer) === call);
117
- }
118
- if (!ts.isIdentifier(node)) return false;
119
- return declarationsOf(ts, checker, node).some(declaration => {
120
- if (ts.isVariableDeclaration(declaration)) return declaration.initializer && verifiedPayload(ts, checker, declaration.initializer, call, next);
121
- if (!ts.isBindingElement(declaration) || (declaration.propertyName ?? declaration.name).getText() !== 'payload') return false;
122
- return unwrap(ts, declaration.parent.parent.initializer) === call;
123
- });
124
- };
125
-
115
+ const node = unwrap(ts, expression);
116
+ if (!node || seen.has(node))
117
+ return false;
118
+ const next = new Set(seen).add(node);
119
+ if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && memberName(ts, node) === 'payload') {
120
+ const value = unwrap(ts, node.expression);
121
+ if (value === call)
122
+ return true;
123
+ if (ts.isIdentifier(value))
124
+ return declarationsOf(ts, checker, value).some(declaration => ts.isVariableDeclaration(declaration) && unwrap(ts, declaration.initializer) === call);
125
+ }
126
+ if (!ts.isIdentifier(node))
127
+ return false;
128
+ return declarationsOf(ts, checker, node).some(declaration => {
129
+ if (ts.isVariableDeclaration(declaration))
130
+ return declaration.initializer && verifiedPayload(ts, checker, declaration.initializer, call, next);
131
+ if (!ts.isBindingElement(declaration) || (declaration.propertyName ?? declaration.name).getText() !== 'payload')
132
+ return false;
133
+ return unwrap(ts, declaration.parent.parent.initializer) === call;
134
+ });
135
+ };
126
136
  const rejects = (ts, statement) => {
127
- if (ts.isBlock(statement)) {
128
- const exit = statement.statements.find(item => ts.isThrowStatement(item) || ts.isReturnStatement(item));
129
- return Boolean(exit && rejects(ts, exit));
130
- }
131
- if (ts.isThrowStatement(statement)) return true;
132
- if (!ts.isReturnStatement(statement)) return false;
133
- const value = statement.expression && unwrap(ts, statement.expression);
134
- return !value || value.kind === ts.SyntaxKind.NullKeyword || value.kind === ts.SyntaxKind.FalseKeyword
135
- || (ts.isCallExpression(value) && /^(?:refused|err|fail|failure)$/u.test(value.expression.getText()));
136
- };
137
-
137
+ if (ts.isBlock(statement)) {
138
+ const exit = statement.statements.find(item => ts.isThrowStatement(item) || ts.isReturnStatement(item));
139
+ return Boolean(exit && rejects(ts, exit));
140
+ }
141
+ if (ts.isThrowStatement(statement))
142
+ return true;
143
+ if (!ts.isReturnStatement(statement))
144
+ return false;
145
+ const value = statement.expression && unwrap(ts, statement.expression);
146
+ return !value || value.kind === ts.SyntaxKind.NullKeyword || value.kind === ts.SyntaxKind.FalseKeyword
147
+ || (ts.isCallExpression(value) && /^(?:refused|err|fail|failure)$/u.test(value.expression.getText()));
148
+ };
138
149
  const roleCondition = (ts, checker, condition, call, equality = false) => {
139
- const node = unwrap(ts, condition);
140
- if (!node || !ts.isBinaryExpression(node)) return false;
141
- const combined = equality ? ts.SyntaxKind.AmpersandAmpersandToken : ts.SyntaxKind.BarBarToken;
142
- if (node.operatorToken.kind === combined) return roleCondition(ts, checker, node.left, call, equality) || roleCondition(ts, checker, node.right, call, equality);
143
- const operators = equality ? [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken]
144
- : [ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken];
145
- if (!operators.includes(node.operatorToken.kind)) return false;
146
- const role = expression => (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression))
147
- && memberName(ts, expression) === 'role' && verifiedPayload(ts, checker, expression.expression, call);
148
- return (role(node.left) && literalString(ts, checker, node.right) === 'authenticated')
149
- || (role(node.right) && literalString(ts, checker, node.left) === 'authenticated');
150
- };
151
-
150
+ const node = unwrap(ts, condition);
151
+ if (!node || !ts.isBinaryExpression(node))
152
+ return false;
153
+ const combined = equality ? ts.SyntaxKind.AmpersandAmpersandToken : ts.SyntaxKind.BarBarToken;
154
+ if (node.operatorToken.kind === combined)
155
+ return roleCondition(ts, checker, node.left, call, equality) || roleCondition(ts, checker, node.right, call, equality);
156
+ const operators = equality ? [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken]
157
+ : [ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken];
158
+ if (!operators.includes(node.operatorToken.kind))
159
+ return false;
160
+ const role = (expression) => (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression))
161
+ && memberName(ts, expression) === 'role' && verifiedPayload(ts, checker, expression.expression, call);
162
+ return (role(node.left) && literalString(ts, checker, node.right) === 'authenticated')
163
+ || (role(node.right) && literalString(ts, checker, node.left) === 'authenticated');
164
+ };
152
165
  const returnsPrincipal = (ts, statement) => {
153
- let accepted = false;
154
- const visit = node => {
155
- if (ts.isFunctionLike(node)) return;
156
- if (ts.isReturnStatement(node) && !rejects(ts, node)) accepted = true;
157
- ts.forEachChild(node, visit);
158
- };
159
- visit(statement);
160
- return accepted;
161
- };
162
-
166
+ let accepted = false;
167
+ const visit = (node) => {
168
+ if (ts.isFunctionLike(node))
169
+ return;
170
+ if (ts.isReturnStatement(node) && !rejects(ts, node))
171
+ accepted = true;
172
+ ts.forEachChild(node, visit);
173
+ };
174
+ visit(statement);
175
+ return accepted;
176
+ };
163
177
  const statementsAfterVerify = (ts, call) => {
164
- let statement = call;
165
- while (statement.parent && !ts.isBlock(statement.parent)) statement = statement.parent;
166
- const block = statement.parent;
167
- if (!block || !ts.isBlock(block)) return [];
168
- return block.statements.slice(block.statements.indexOf(statement) + 1);
178
+ let statement = call;
179
+ while (statement.parent && !ts.isBlock(statement.parent))
180
+ statement = statement.parent;
181
+ const block = statement.parent;
182
+ if (!block || !ts.isBlock(block))
183
+ return [];
184
+ const siblings = block.statements;
185
+ return block.statements.slice(siblings.indexOf(statement) + 1);
169
186
  };
170
-
171
187
  const rejectsRoleGuard = (ts, checker, statement, call) => {
172
- if (!ts.isIfStatement(statement)) return false;
173
- if (roleCondition(ts, checker, statement.expression, call) && rejects(ts, statement.thenStatement)) return true;
174
- return Boolean(statement.elseStatement && roleCondition(ts, checker, statement.expression, call, true) && rejects(ts, statement.elseStatement));
188
+ if (!ts.isIfStatement(statement))
189
+ return false;
190
+ if (roleCondition(ts, checker, statement.expression, call) && rejects(ts, statement.thenStatement))
191
+ return true;
192
+ return Boolean(statement.elseStatement && roleCondition(ts, checker, statement.expression, call, true) && rejects(ts, statement.elseStatement));
175
193
  };
176
-
177
194
  const rejectsOtherRoles = (ts, checker, call) => {
178
- for (const item of statementsAfterVerify(ts, call)) {
179
- if (ts.isReturnStatement(item) || ts.isThrowStatement(item)) return false;
180
- if (rejectsRoleGuard(ts, checker, item, call)) return true;
181
- if (returnsPrincipal(ts, item)) return false;
182
- }
183
- return false;
184
- };
185
-
195
+ for (const item of statementsAfterVerify(ts, call)) {
196
+ if (ts.isReturnStatement(item) || ts.isThrowStatement(item))
197
+ return false;
198
+ if (rejectsRoleGuard(ts, checker, item, call))
199
+ return true;
200
+ if (returnsPrincipal(ts, item))
201
+ return false;
202
+ }
203
+ return false;
204
+ };
186
205
  /** R260 is shared by the architecture machine and the published typed ESLint rule. */
187
206
  export const jwtPolicyProblems = (ts, checker, call) => {
188
- const problems = [];
189
- const options = objectValue(ts, checker, call.arguments[2]);
190
- const property = name => options?.properties.findLast(entry =>
191
- (ts.isPropertyAssignment(entry) || ts.isShorthandPropertyAssignment(entry))
192
- && (ts.isIdentifier(entry.name) || ts.isStringLiteralLike(entry.name)) && entry.name.text === name);
193
- const value = name => {
194
- const entry = property(name);
195
- return entry && unwrap(ts, ts.isPropertyAssignment(entry) ? entry.initializer : entry.name);
196
- };
197
- const algorithms = value('algorithms');
198
- const secure = new Set(['ES256', 'RS256', 'EdDSA']);
199
- if (!algorithms || !ts.isArrayLiteralExpression(algorithms) || !algorithms.elements.length
200
- || algorithms.elements.some(item => !ts.isStringLiteralLike(item) || !secure.has(item.text))
201
- || options?.properties.some(entry => ts.isSpreadAssignment(entry))) {
202
- problems.push('BE_JWT_ALG_PINNED: algorithms must be a non-empty literal subset of ES256, RS256 and EdDSA.');
203
- }
204
- if (!fromConfig(ts, checker, value('issuer'))) problems.push('BE_JWT_ALG_PINNED: issuer must derive from typed configuration.');
205
- // The principal is bound either by an audience of typed configuration (an OIDC client such as Keycloak, the app's own
206
- // Passport tokens) or, where the audience is shared by every token of the issuer (Supabase's literal 'authenticated'),
207
- // by rejecting every role other than authenticated before the principal is returned.
208
- if (!fromConfig(ts, checker, value('audience')) && !rejectsOtherRoles(ts, checker, call)) {
209
- problems.push('BE_JWT_ALG_PINNED: an OIDC verifier audience must derive from typed configuration, or the verified principal must reject every role other than authenticated before returning it.');
210
- }
211
- return problems;
207
+ const problems = [];
208
+ const options = objectValue(ts, checker, call.arguments[2]);
209
+ const property = (name) => options?.properties.findLast((entry) => (ts.isPropertyAssignment(entry) || ts.isShorthandPropertyAssignment(entry))
210
+ && (ts.isIdentifier(entry.name) || ts.isStringLiteralLike(entry.name)) && entry.name.text === name);
211
+ const value = (name) => {
212
+ const entry = property(name);
213
+ return entry && unwrap(ts, ts.isPropertyAssignment(entry) ? entry.initializer : entry.name);
214
+ };
215
+ const algorithms = value('algorithms');
216
+ const secure = new Set(['ES256', 'RS256', 'EdDSA']);
217
+ if (!algorithms || !ts.isArrayLiteralExpression(algorithms) || !algorithms.elements.length
218
+ || algorithms.elements.some(item => !ts.isStringLiteralLike(item) || !secure.has(item.text))
219
+ || options?.properties.some(entry => ts.isSpreadAssignment(entry))) {
220
+ problems.push('BE_JWT_ALG_PINNED: algorithms must be a non-empty literal subset of ES256, RS256 and EdDSA.');
221
+ }
222
+ if (!fromConfig(ts, checker, value('issuer')))
223
+ problems.push('BE_JWT_ALG_PINNED: issuer must derive from typed configuration.');
224
+ // The principal is bound either by an audience of typed configuration (an OIDC client such as Keycloak, the app's own
225
+ // Passport tokens) or, where the audience is shared by every token of the issuer (Supabase's literal 'authenticated'),
226
+ // by rejecting every role other than authenticated before the principal is returned.
227
+ if (!fromConfig(ts, checker, value('audience')) && !rejectsOtherRoles(ts, checker, call)) {
228
+ problems.push('BE_JWT_ALG_PINNED: an OIDC verifier audience must derive from typed configuration, or the verified principal must reject every role other than authenticated before returning it.');
229
+ }
230
+ return problems;
212
231
  };