@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.
- package/CHANGELOG.md +12 -0
- package/docs/button-intent.md +1 -1
- package/package.json +1 -1
- package/runtime/bundle.json +192 -0
- package/runtime/engine/by-code-unit.mjs +6 -6
- package/runtime/engine/model-config.mjs +98 -76
- package/runtime/engine/orca-config.mjs +22 -17
- package/runtime/engine/release-config.mjs +22 -18
- package/runtime/engine/removed-vocabulary.mjs +21 -20
- package/runtime/engine/resources-config.mjs +42 -35
- package/runtime/engine/runtime-root.mjs +11 -13
- package/runtime/engine/secrets.mjs +170 -102
- package/runtime/engine/sonar-config.mjs +16 -13
- package/runtime/engine/temp-root.mjs +3 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -1
- package/runtime/scripts/api/git/cat-file.mjs +0 -1
- package/runtime/scripts/api/git/config-get.mjs +1 -2
- package/runtime/scripts/api/git/diff.mjs +0 -1
- package/runtime/scripts/api/git/lib.mjs +32 -32
- package/runtime/scripts/api/git/ls-files.mjs +0 -1
- package/runtime/scripts/api/git/ls-tree.mjs +0 -1
- package/runtime/scripts/api/git/merge-base.mjs +2 -3
- package/runtime/scripts/api/git/remote.mjs +0 -1
- package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
- package/runtime/scripts/api/git/show.mjs +0 -1
- package/runtime/scripts/hfs/allows.mjs +37 -38
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
- package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
- package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
- package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
- package/runtime/scripts/hfs/architecture/config.mjs +302 -268
- package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
- package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
- package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
- package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
- package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
- package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
- package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
- package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
- package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
- package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
- package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
- package/runtime/scripts/hfs/architecture/index.mjs +272 -272
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
- package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
- package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
- package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
- package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
- package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
- package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
- package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
- package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
- package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
- package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
- package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
- package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
- package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
- package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
- package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
- package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
- package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
- package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
- package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
- package/runtime/scripts/hfs/edition-slots.mjs +43 -40
- package/runtime/scripts/hfs/linear-text.mjs +20 -20
- package/runtime/scripts/hfs/param-names.mjs +6 -11
- package/runtime/scripts/hfs/path-findings.mjs +120 -100
- package/runtime/scripts/hfs/repo-identity.mjs +55 -42
- package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
- package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
- package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
- package/runtime/scripts/hfs/rules/read.mjs +19 -15
- package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
- package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
- package/runtime/scripts/hfs/slot-classify.mjs +42 -38
- package/runtime/scripts/hfs/slot-errors.mjs +8 -7
- package/runtime/scripts/hfs/slot-imports.mjs +70 -63
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
- package/runtime/scripts/hfs/slot-match.mjs +122 -111
- package/runtime/scripts/hfs/slot-path.mjs +0 -2
- package/runtime/scripts/hfs/slot-required.mjs +65 -59
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
- package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
- package/runtime/scripts/hfs/slots.mjs +175 -152
- package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
- package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
- package/runtime/scripts/hfs/view.mjs +38 -40
- package/runtime/scripts/lib/dockerfile.mjs +131 -114
- package/runtime/scripts/lib/env.mjs +3 -6
- package/runtime/scripts/lib/event-contract.mjs +73 -78
- package/runtime/scripts/lib/git.mjs +15 -16
- package/runtime/scripts/lib/glob.mjs +23 -14
- package/runtime/scripts/lib/grammar-lint.mjs +33 -37
- package/runtime/scripts/lib/i18n.mjs +32 -35
- package/runtime/scripts/lib/in-order.mjs +57 -38
- package/runtime/scripts/lib/json-clone.mjs +45 -36
- package/runtime/scripts/lib/json.mjs +26 -14
- package/runtime/scripts/lib/language.mjs +133 -129
- package/runtime/scripts/lib/list.mjs +9 -14
- package/runtime/scripts/lib/mutation-fence.mjs +10 -5
- package/runtime/scripts/lib/path-key.mjs +53 -38
- package/runtime/scripts/lib/read-text.mjs +30 -17
- package/runtime/scripts/lib/regex.mjs +1 -2
- package/runtime/scripts/lib/same-text.mjs +0 -1
- package/runtime/scripts/lib/sops-envelope.mjs +148 -120
- package/runtime/scripts/lib/stack-services.mjs +128 -129
- package/runtime/scripts/lib/ts-analysis.mjs +28 -26
- package/runtime/scripts/lib/ts-ast.mjs +36 -42
- package/runtime/scripts/lib/walk.mjs +33 -27
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
violations.push({ ruleId, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
45
45
|
};
|
|
46
|
-
|
|
47
46
|
const declarationsOf = (ts, checker, node) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (ts.
|
|
121
|
-
|
|
122
|
-
return
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
};
|