@starci/hfs 4.0.9 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +247 -273
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/lib.mjs +6 -0
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +90 -45
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +103 -2
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
|
@@ -19,111 +19,136 @@ const ORIGIN_HEADERS = new Set(['origin', 'referer', 'referrer']);
|
|
|
19
19
|
const METADATA_READERS = new Set(['get', 'getAll', 'getAllAndMerge', 'getAllAndOverride']);
|
|
20
20
|
const LABEL = { throttler: 'the throttler guard', csrf: 'the CSRF origin guard of platform/http-security', auth: 'AuthGuard of domain/identity' };
|
|
21
21
|
|
|
22
|
+
const keySymbol = (kit, checker, node) => (node ? kit.aliased(checker, kit.symbolAt(checker, node)) ?? null : null);
|
|
23
|
+
|
|
24
|
+
const extendsThrottlerType = (kit, checker, declaration, type, depth) => {
|
|
25
|
+
if (kit.isImportOf(checker, type.expression, 'ThrottlerGuard', '@nestjs/throttler')) return true;
|
|
26
|
+
return kit.declarationsOf(checker, type.expression).some(parent => parent !== declaration && extendsThrottler(kit, parent, depth + 1));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function extendsThrottler(kit, declaration, depth = 0) {
|
|
30
|
+
if (!kit.ts.isClassDeclaration(declaration) || depth > 8) return false;
|
|
31
|
+
const checker = kit.checkerOf(declaration.getSourceFile());
|
|
32
|
+
if (!checker) return false;
|
|
33
|
+
const types = (declaration.heritageClauses ?? []).flatMap(clause => [...clause.types]);
|
|
34
|
+
return types.some(type => extendsThrottlerType(kit, checker, declaration, type, depth));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const collectSetMetadataKeys = (kit, file, keys) => {
|
|
38
|
+
const { ts } = kit;
|
|
39
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
40
|
+
if (!checker) return;
|
|
41
|
+
kit.walk(file.sourceFile, node => {
|
|
42
|
+
if (ts.isCallExpression(node) && kit.isImportOf(checker, node.expression, 'SetMetadata', '@nestjs/common')) {
|
|
43
|
+
const key = keySymbol(kit, checker, node.arguments[0]);
|
|
44
|
+
if (key) keys.add(key);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The symbols a SetMetadata call of the owner writes as metadata keys: what its door decorators (`@RateLimit(tier)`) attach. */
|
|
51
|
+
const writtenKeys = (kit, graph, owner, cache) => {
|
|
52
|
+
if (cache.has(owner.root)) return cache.get(owner.root);
|
|
53
|
+
const keys = new Set();
|
|
54
|
+
for (const file of graph.files.values()) {
|
|
55
|
+
if (file.owner?.root === owner.root) collectSetMetadataKeys(kit, file, keys);
|
|
56
|
+
}
|
|
57
|
+
cache.set(owner.root, keys);
|
|
58
|
+
return keys;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** True when the class reads a request origin or referer header (the origin/referer allowlist of the CSRF guard). */
|
|
62
|
+
const readsOrigin = (kit, declaration) => {
|
|
63
|
+
const { ts } = kit;
|
|
64
|
+
let found = false;
|
|
65
|
+
kit.walk(declaration, node => {
|
|
66
|
+
let name = null;
|
|
67
|
+
if (ts.isPropertyAccessExpression(node)) name = node.name.text;
|
|
68
|
+
else if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) name = node.argumentExpression.text;
|
|
69
|
+
if (name && ORIGIN_HEADERS.has(name.toLowerCase())) found = true;
|
|
70
|
+
return !found;
|
|
71
|
+
});
|
|
72
|
+
return found;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isMetadataRead = (ts, node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && METADATA_READERS.has(node.expression.name.text);
|
|
76
|
+
|
|
77
|
+
/** True when the class reads, through a metadata getter, a key its own capability's SetMetadata decorators write. */
|
|
78
|
+
const readsDoorMetadata = (kit, graph, keyCache, declaration, owner) => {
|
|
79
|
+
const checker = kit.checkerOf(declaration.getSourceFile());
|
|
80
|
+
if (!checker) return false;
|
|
81
|
+
const keys = writtenKeys(kit, graph, owner, keyCache);
|
|
82
|
+
let found = false;
|
|
83
|
+
kit.walk(declaration, node => {
|
|
84
|
+
if (isMetadataRead(kit.ts, node)) {
|
|
85
|
+
const key = keySymbol(kit, checker, node.arguments[0]);
|
|
86
|
+
if (key && keys.has(key)) found = true;
|
|
87
|
+
}
|
|
88
|
+
return !found;
|
|
89
|
+
});
|
|
90
|
+
return found;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const classifyDeclaration = (kit, graph, keyCache, declaration) => {
|
|
94
|
+
const { ts } = kit;
|
|
95
|
+
const owner = kit.ownerOfDeclaration(declaration);
|
|
96
|
+
if (owner?.tier === 'platform' && owner.name === 'http-security' && ts.isClassDeclaration(declaration)) {
|
|
97
|
+
if (readsOrigin(kit, declaration)) return 'csrf';
|
|
98
|
+
if (readsDoorMetadata(kit, graph, keyCache, declaration, owner)) return 'throttler';
|
|
99
|
+
}
|
|
100
|
+
if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'AuthGuard') return 'auth';
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const classify = (kit, graph, keyCache, checker, node) => {
|
|
105
|
+
if (!node) return null;
|
|
106
|
+
if (kit.importBinding(checker, node)?.module === '@nestjs/throttler') return 'throttler';
|
|
107
|
+
const declarations = kit.declarationsOf(checker, node);
|
|
108
|
+
if (declarations.some(declaration => extendsThrottler(kit, declaration))) return 'throttler';
|
|
109
|
+
for (const declaration of declarations) {
|
|
110
|
+
const role = classifyDeclaration(kit, graph, keyCache, declaration);
|
|
111
|
+
if (role) return role;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const roleMessages = (app, roles, anchor) => {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const role of ORDER) {
|
|
119
|
+
const entries = roles.filter(item => item.role === role);
|
|
120
|
+
if (entries.length === 0) out.push({ node: anchor, message: `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then AuthGuard.` });
|
|
121
|
+
else if (entries.length > 1) out.push({ node: entries[1].guard.node, message: `App ${app.name} provides ${LABEL[role]} more than once as an APP_GUARD.` });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const orderMessages = (app, roles) => {
|
|
127
|
+
const indexes = ORDER.map(role => roles.findIndex(item => item.role === role));
|
|
128
|
+
if (!indexes.every(index => index >= 0)) return [];
|
|
129
|
+
const out = [];
|
|
130
|
+
for (let i = 1; i < indexes.length; i += 1) {
|
|
131
|
+
if (indexes[i] < indexes[i - 1]) out.push({ node: roles[indexes[i]].guard.node, message: `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, AuthGuard.` });
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
|
|
22
136
|
export function checkDefaultDeny(input) {
|
|
23
137
|
const { config } = input;
|
|
24
138
|
const kit = machineKit(input);
|
|
25
|
-
const { ts } = kit;
|
|
26
139
|
const violations = [];
|
|
27
|
-
let apps = 0;
|
|
28
|
-
|
|
29
|
-
const extendsThrottler = (declaration, depth = 0) => {
|
|
30
|
-
if (!ts.isClassDeclaration(declaration) || depth > 8) return false;
|
|
31
|
-
const checker = kit.checkerOf(declaration.getSourceFile());
|
|
32
|
-
if (!checker) return false;
|
|
33
|
-
for (const clause of declaration.heritageClauses ?? []) {
|
|
34
|
-
for (const type of clause.types) {
|
|
35
|
-
if (kit.isImportOf(checker, type.expression, 'ThrottlerGuard', '@nestjs/throttler')) return true;
|
|
36
|
-
if (kit.declarationsOf(checker, type.expression).some(parent => parent !== declaration && extendsThrottler(parent, depth + 1))) return true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const keySymbol = (checker, node) => (node ? kit.aliased(checker, kit.symbolAt(checker, node)) ?? null : null);
|
|
43
|
-
|
|
44
|
-
/** The symbols a SetMetadata call of the owner writes as metadata keys: what its door decorators (`@RateLimit(tier)`) attach. */
|
|
45
|
-
const writtenKeys = (owner, cache = new Map()) => {
|
|
46
|
-
if (cache.has(owner.root)) return cache.get(owner.root);
|
|
47
|
-
const keys = new Set();
|
|
48
|
-
for (const file of input.graph.files.values()) {
|
|
49
|
-
if (file.owner?.root !== owner.root) continue;
|
|
50
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
51
|
-
if (!checker) continue;
|
|
52
|
-
kit.walk(file.sourceFile, node => {
|
|
53
|
-
if (ts.isCallExpression(node) && kit.isImportOf(checker, node.expression, 'SetMetadata', '@nestjs/common')) {
|
|
54
|
-
const key = keySymbol(checker, node.arguments[0]);
|
|
55
|
-
if (key) keys.add(key);
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
cache.set(owner.root, keys);
|
|
61
|
-
return keys;
|
|
62
|
-
};
|
|
63
140
|
const keyCache = new Map();
|
|
64
|
-
|
|
65
|
-
/** True when the class reads a request origin or referer header (the origin/referer allowlist of the CSRF guard). */
|
|
66
|
-
const readsOrigin = declaration => {
|
|
67
|
-
let found = false;
|
|
68
|
-
kit.walk(declaration, node => {
|
|
69
|
-
if (ts.isPropertyAccessExpression(node) && ORIGIN_HEADERS.has(node.name.text.toLowerCase())) found = true;
|
|
70
|
-
else if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression) && ORIGIN_HEADERS.has(node.argumentExpression.text.toLowerCase())) found = true;
|
|
71
|
-
return !found;
|
|
72
|
-
});
|
|
73
|
-
return found;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** True when the class reads, through a metadata getter, a key its own capability's SetMetadata decorators write. */
|
|
77
|
-
const readsDoorMetadata = (declaration, owner) => {
|
|
78
|
-
const checker = kit.checkerOf(declaration.getSourceFile());
|
|
79
|
-
if (!checker) return false;
|
|
80
|
-
const keys = writtenKeys(owner, keyCache);
|
|
81
|
-
let found = false;
|
|
82
|
-
kit.walk(declaration, node => {
|
|
83
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && METADATA_READERS.has(node.expression.name.text)) {
|
|
84
|
-
const key = keySymbol(checker, node.arguments[0]);
|
|
85
|
-
if (key && keys.has(key)) found = true;
|
|
86
|
-
}
|
|
87
|
-
return !found;
|
|
88
|
-
});
|
|
89
|
-
return found;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const classify = (checker, node) => {
|
|
93
|
-
if (!node) return null;
|
|
94
|
-
if (kit.importBinding(checker, node)?.module === '@nestjs/throttler') return 'throttler';
|
|
95
|
-
const declarations = kit.declarationsOf(checker, node);
|
|
96
|
-
if (declarations.some(declaration => extendsThrottler(declaration))) return 'throttler';
|
|
97
|
-
for (const declaration of declarations) {
|
|
98
|
-
const owner = kit.ownerOfDeclaration(declaration);
|
|
99
|
-
if (owner?.tier === 'platform' && owner.name === 'http-security' && ts.isClassDeclaration(declaration)) {
|
|
100
|
-
if (readsOrigin(declaration)) return 'csrf';
|
|
101
|
-
if (readsDoorMetadata(declaration, owner)) return 'throttler';
|
|
102
|
-
}
|
|
103
|
-
if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'AuthGuard') return 'auth';
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
};
|
|
141
|
+
let apps = 0;
|
|
107
142
|
|
|
108
143
|
for (const app of config.apps.filter(item => item.kind === 'api')) {
|
|
109
144
|
const root = kit.appRoot(app.name);
|
|
110
145
|
if (!root) continue;
|
|
111
146
|
apps += 1;
|
|
112
147
|
const guards = kit.providersOf(root, 'APP_GUARD', '@nestjs/core');
|
|
113
|
-
const roles = guards.map(guard => ({ guard, role: classify(guard.checker, guard.useClass) }));
|
|
148
|
+
const roles = guards.map(guard => ({ guard, role: classify(kit, input.graph, keyCache, guard.checker, guard.useClass) }));
|
|
114
149
|
const anchor = guards[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
|
|
115
|
-
const
|
|
116
|
-
for (const
|
|
117
|
-
const entries = roles.filter(item => item.role === role);
|
|
118
|
-
if (entries.length === 0) finding(anchor, `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then AuthGuard.`);
|
|
119
|
-
else if (entries.length > 1) finding(entries[1].guard.node, `App ${app.name} provides ${LABEL[role]} more than once as an APP_GUARD.`);
|
|
120
|
-
}
|
|
121
|
-
const indexes = ORDER.map(role => roles.findIndex(item => item.role === role));
|
|
122
|
-
if (indexes.every(index => index >= 0)) {
|
|
123
|
-
for (let i = 1; i < indexes.length; i += 1) {
|
|
124
|
-
if (indexes[i] < indexes[i - 1]) finding(roles[indexes[i]].guard.node, `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, AuthGuard.`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
150
|
+
const messages = [...roleMessages(app, roles, anchor), ...orderMessages(app, roles)];
|
|
151
|
+
for (const { node, message } of messages) violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, node), app: app.name, message });
|
|
127
152
|
}
|
|
128
153
|
return { violations, coverage: { status: 'checked', apps } };
|
|
129
154
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
4
|
import { documentLanguageHits, isDocument, isLocalizedDataFile } from '../../lib/language.mjs';
|
|
5
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* R96 `doc-language` (HFS_DOC_NOT_ENGLISH). Every Markdown and YAML document under knowledge/, docs/, src/ and apps/ is English:
|
|
@@ -23,7 +24,7 @@ export function checkDocLanguage({ config, graph }) {
|
|
|
23
24
|
const tree = treeOf(config.root);
|
|
24
25
|
const violations = [];
|
|
25
26
|
let files = 0;
|
|
26
|
-
for (const file of [...tree.files].sort()) {
|
|
27
|
+
for (const file of [...tree.files].sort(byCodeUnit)) {
|
|
27
28
|
if (!DOCUMENT_ROOTS.includes(file.split('/')[0]) || !isDocument(file) || file.split('/').includes('node_modules')) continue;
|
|
28
29
|
const classified = resolver.classifyPath(file);
|
|
29
30
|
if (isLocalizedDataFile(file, classified.status === 'owned' ? classified.slot : null)) continue;
|
|
@@ -16,43 +16,58 @@ export const ENTRYPOINT_RULE_IDS = ['BE_ENTRYPOINT_ONLY_IN_APPS'];
|
|
|
16
16
|
|
|
17
17
|
const RULE = 'BE_ENTRYPOINT_ONLY_IN_APPS';
|
|
18
18
|
|
|
19
|
+
const TRANSPARENT = ['isVoidExpression', 'isAwaitExpression', 'isParenthesizedExpression', 'isNonNullExpression'];
|
|
20
|
+
|
|
21
|
+
/** The identifier a top-level statement calls: `bootstrap()`, `void bootstrap()`, `bootstrap().catch(...)`. */
|
|
22
|
+
function calledIdentifier(ts, expression) {
|
|
23
|
+
let current = expression;
|
|
24
|
+
for (;;) {
|
|
25
|
+
if (TRANSPARENT.some(test => ts[test](current))) current = current.expression;
|
|
26
|
+
else if (ts.isCallExpression(current)) {
|
|
27
|
+
if (ts.isIdentifier(current.expression)) return current.expression;
|
|
28
|
+
current = current.expression;
|
|
29
|
+
} else if (ts.isPropertyAccessExpression(current)) current = current.expression;
|
|
30
|
+
else return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isNestFactoryCreate = (kit, ts, checker, node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text.startsWith('create')
|
|
35
|
+
&& kit.isImportOf(checker, node.expression.expression, 'NestFactory', '@nestjs/core');
|
|
36
|
+
|
|
37
|
+
const isMainFile = file => (Boolean(file.slot?.startsWith('be.app.')) && path.posix.basename(file.rel) === 'main.ts') || isTestWorldSlot(file.slot);
|
|
38
|
+
|
|
39
|
+
/** Counts the NestFactory.create* calls of an entrypoint owner and reports them in any other file. */
|
|
40
|
+
function inspectFactoryCalls(kit, file, isMain, report) {
|
|
41
|
+
const { ts } = kit;
|
|
42
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
43
|
+
let entrypoints = 0;
|
|
44
|
+
kit.walk(file.sourceFile, node => {
|
|
45
|
+
if (!isNestFactoryCreate(kit, ts, checker, node)) return;
|
|
46
|
+
if (isMain) entrypoints += 1;
|
|
47
|
+
else report(node, `NestFactory.${node.expression.name.text}() starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.`);
|
|
48
|
+
});
|
|
49
|
+
return entrypoints;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inspectBootstrapCalls(ts, file, report) {
|
|
53
|
+
for (const statement of file.sourceFile.statements) {
|
|
54
|
+
if (!ts.isExpressionStatement(statement)) continue;
|
|
55
|
+
if (calledIdentifier(ts, statement.expression)?.text === 'bootstrap') {
|
|
56
|
+
report(statement, "A top-level bootstrap() call starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
19
61
|
export function checkEntrypoints(input) {
|
|
20
62
|
const { graph } = input;
|
|
21
63
|
const kit = machineKit(input);
|
|
22
|
-
const { ts } = kit;
|
|
23
64
|
const violations = [];
|
|
24
65
|
let entrypoints = 0;
|
|
25
|
-
/** The identifier a top-level statement calls: `bootstrap()`, `void bootstrap()`, `bootstrap().catch(...)`. */
|
|
26
|
-
const calledIdentifier = expression => {
|
|
27
|
-
let current = expression;
|
|
28
|
-
for (;;) {
|
|
29
|
-
if (ts.isVoidExpression(current) || ts.isAwaitExpression(current) || ts.isParenthesizedExpression(current) || ts.isNonNullExpression(current)) current = current.expression;
|
|
30
|
-
else if (ts.isCallExpression(current)) {
|
|
31
|
-
if (ts.isIdentifier(current.expression)) return current.expression;
|
|
32
|
-
current = current.expression;
|
|
33
|
-
} else if (ts.isPropertyAccessExpression(current)) current = current.expression;
|
|
34
|
-
else return null;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
66
|
for (const file of graph.files.values()) {
|
|
38
|
-
const isMain = (
|
|
39
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
67
|
+
const isMain = isMainFile(file);
|
|
40
68
|
const report = (node, message) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message });
|
|
41
|
-
kit
|
|
42
|
-
|
|
43
|
-
&& kit.isImportOf(checker, node.expression.expression, 'NestFactory', '@nestjs/core')) {
|
|
44
|
-
if (isMain) entrypoints += 1;
|
|
45
|
-
else report(node, `NestFactory.${node.expression.name.text}() starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.`);
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
});
|
|
49
|
-
if (isMain) continue;
|
|
50
|
-
for (const statement of file.sourceFile.statements) {
|
|
51
|
-
if (!ts.isExpressionStatement(statement)) continue;
|
|
52
|
-
if (calledIdentifier(statement.expression)?.text === 'bootstrap') {
|
|
53
|
-
report(statement, "A top-level bootstrap() call starts a process outside apps/<app>/src/main.ts; entrypoints live in an app's main.ts only.");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
69
|
+
entrypoints += inspectFactoryCalls(kit, file, isMain, report);
|
|
70
|
+
if (!isMain) inspectBootstrapCalls(kit.ts, file, report);
|
|
56
71
|
}
|
|
57
72
|
return { violations, coverage: { status: 'checked', entrypoints } };
|
|
58
73
|
}
|
|
@@ -11,6 +11,24 @@ export const ERROR_CODE_RULE_IDS = ['BE_ERROR_HOME'];
|
|
|
11
11
|
|
|
12
12
|
const RULE = 'BE_ERROR_HOME';
|
|
13
13
|
|
|
14
|
+
// Judge each member of one <C>ErrorCode enum; returns how many string-literal codes it held.
|
|
15
|
+
const checkEnumCodes = (statement, { file, capability, shape, kit, ts, seen, violations }) => {
|
|
16
|
+
let checked = 0;
|
|
17
|
+
for (const member of statement.members) {
|
|
18
|
+
const report = message => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, member), message });
|
|
19
|
+
const name = kit.propertyNameText(member.name) ?? '?';
|
|
20
|
+
const value = member.initializer && ts.isStringLiteralLike(member.initializer) ? member.initializer.text : null;
|
|
21
|
+
if (value === null) { report(`${statement.name.text}.${name} must be a string literal code (\`${capability}_<WHAT>\`); a computed or numeric code cannot be checked or told apart in a log.`); continue; }
|
|
22
|
+
checked += 1;
|
|
23
|
+
if (!shape.test(value) || /_(?:ERROR|EXCEPTION)$/u.test(value)) {
|
|
24
|
+
report(`${statement.name.text}.${name} = "${value}" must match ${capability}_<WHAT> in UPPER_SNAKE with the capability prefix and no _ERROR or _EXCEPTION suffix.`);
|
|
25
|
+
}
|
|
26
|
+
if (seen.has(value)) report(`Error code "${value}" is already declared in ${seen.get(value)}; a code is unique across the repository.`);
|
|
27
|
+
else seen.set(value, file.rel);
|
|
28
|
+
}
|
|
29
|
+
return checked;
|
|
30
|
+
};
|
|
31
|
+
|
|
14
32
|
export function checkErrorCodes(input) {
|
|
15
33
|
const { graph } = input;
|
|
16
34
|
const kit = machineKit(input);
|
|
@@ -27,18 +45,7 @@ export function checkErrorCodes(input) {
|
|
|
27
45
|
for (const statement of file.sourceFile.statements) {
|
|
28
46
|
if (!ts.isEnumDeclaration(statement) || !statement.name.text.endsWith('ErrorCode')) continue;
|
|
29
47
|
enums += 1;
|
|
30
|
-
|
|
31
|
-
const report = message => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, member), message });
|
|
32
|
-
const name = kit.propertyNameText(member.name) ?? '?';
|
|
33
|
-
const value = member.initializer && ts.isStringLiteralLike(member.initializer) ? member.initializer.text : null;
|
|
34
|
-
if (value === null) { report(`${statement.name.text}.${name} must be a string literal code (\`${capability}_<WHAT>\`); a computed or numeric code cannot be checked or told apart in a log.`); continue; }
|
|
35
|
-
codes += 1;
|
|
36
|
-
if (!shape.test(value) || /_(?:ERROR|EXCEPTION)$/u.test(value)) {
|
|
37
|
-
report(`${statement.name.text}.${name} = "${value}" must match ${capability}_<WHAT> in UPPER_SNAKE with the capability prefix and no _ERROR or _EXCEPTION suffix.`);
|
|
38
|
-
}
|
|
39
|
-
if (seen.has(value)) report(`Error code "${value}" is already declared in ${seen.get(value)}; a code is unique across the repository.`);
|
|
40
|
-
else seen.set(value, file.rel);
|
|
41
|
-
}
|
|
48
|
+
codes += checkEnumCodes(statement, { file, capability, shape, kit, ts, seen, violations });
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
return { violations, coverage: { status: 'checked', enums, codes } };
|
|
@@ -10,50 +10,63 @@ export const ERROR_MASKED_RULE_IDS = ['BE_ERROR_MASKED'];
|
|
|
10
10
|
|
|
11
11
|
const RULE = 'BE_ERROR_MASKED';
|
|
12
12
|
|
|
13
|
+
const inErrorsOf = kit => (checker, node) => kit.declarationsOf(checker, node).map(kit.ownerOfDeclaration).some(owner => owner?.tier === 'platform' && owner.name === 'errors');
|
|
14
|
+
|
|
15
|
+
function appFilterViolations(kit, inErrors, app, root) {
|
|
16
|
+
const violations = [];
|
|
17
|
+
const filters = kit.providersOf(root, 'APP_FILTER', '@nestjs/core');
|
|
18
|
+
const anchor = filters[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
|
|
19
|
+
if (filters.length !== 1) {
|
|
20
|
+
violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, anchor), app: app.name,
|
|
21
|
+
message: `App ${app.name} provides ${filters.length} APP_FILTER entries; an api app provides exactly one, the ExceptionFilter of platform/errors.` });
|
|
22
|
+
}
|
|
23
|
+
for (const filter of filters) {
|
|
24
|
+
if (!filter.useClass || !inErrors(filter.checker, filter.useClass)) {
|
|
25
|
+
violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, filter.node), app: app.name,
|
|
26
|
+
message: 'The APP_FILTER must be `useClass` of the filter declared in platform/errors; any other filter can let an undeclared error reach the client unmasked.' });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return violations;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isGraphqlRegistration = (kit, checker, node) => kit.ts.isCallExpression(node) && kit.ts.isPropertyAccessExpression(node.expression)
|
|
33
|
+
&& ['forRoot', 'forRootAsync'].includes(node.expression.name.text)
|
|
34
|
+
&& kit.isImportOf(checker, node.expression.expression, 'GraphQLModule', '@nestjs/graphql');
|
|
35
|
+
|
|
36
|
+
function passesMaskedFormatError(kit, inErrors, checker, call) {
|
|
37
|
+
const { ts } = kit;
|
|
38
|
+
let masked = false;
|
|
39
|
+
for (const argument of call.arguments) {
|
|
40
|
+
kit.walk(argument, inner => {
|
|
41
|
+
if ((ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) && kit.propertyNameText(inner.name) === 'formatError'
|
|
42
|
+
&& inErrors(checker, kit.valueOfProperty(inner))) masked = true;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return masked;
|
|
46
|
+
}
|
|
47
|
+
|
|
13
48
|
export function checkErrorMasked(input) {
|
|
14
49
|
const { config, graph } = input;
|
|
15
50
|
const kit = machineKit(input);
|
|
16
|
-
const { ts } = kit;
|
|
17
51
|
const violations = [];
|
|
18
|
-
const inErrors = (
|
|
52
|
+
const inErrors = inErrorsOf(kit);
|
|
19
53
|
let apps = 0;
|
|
20
54
|
for (const app of config.apps.filter(item => item.kind === 'api')) {
|
|
21
55
|
const root = kit.appRoot(app.name);
|
|
22
56
|
if (!root) continue;
|
|
23
57
|
apps += 1;
|
|
24
|
-
|
|
25
|
-
const anchor = filters[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
|
|
26
|
-
if (filters.length !== 1) {
|
|
27
|
-
violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, anchor), app: app.name,
|
|
28
|
-
message: `App ${app.name} provides ${filters.length} APP_FILTER entries; an api app provides exactly one, the ExceptionFilter of platform/errors.` });
|
|
29
|
-
}
|
|
30
|
-
for (const filter of filters) {
|
|
31
|
-
if (!filter.useClass || !inErrors(filter.checker, filter.useClass)) {
|
|
32
|
-
violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, filter.node), app: app.name,
|
|
33
|
-
message: 'The APP_FILTER must be `useClass` of the filter declared in platform/errors; any other filter can let an undeclared error reach the client unmasked.' });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
58
|
+
violations.push(...appFilterViolations(kit, inErrors, app, root));
|
|
36
59
|
}
|
|
37
60
|
let graphql = 0;
|
|
38
61
|
for (const file of graph.files.values()) {
|
|
39
62
|
const checker = kit.checkerOf(file.sourceFile);
|
|
40
63
|
kit.walk(file.sourceFile, node => {
|
|
41
|
-
if (!(
|
|
42
|
-
&& kit.isImportOf(checker, node.expression.expression, 'GraphQLModule', '@nestjs/graphql'))) return true;
|
|
64
|
+
if (!isGraphqlRegistration(kit, checker, node)) return;
|
|
43
65
|
graphql += 1;
|
|
44
|
-
|
|
45
|
-
for (const argument of node.arguments) {
|
|
46
|
-
kit.walk(argument, inner => {
|
|
47
|
-
if ((ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) && kit.propertyNameText(inner.name) === 'formatError'
|
|
48
|
-
&& inErrors(checker, kit.valueOfProperty(inner))) masked = true;
|
|
49
|
-
return true;
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
if (!masked) {
|
|
66
|
+
if (!passesMaskedFormatError(kit, inErrors, checker, node)) {
|
|
53
67
|
violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node),
|
|
54
68
|
message: `GraphQLModule.${node.expression.name.text} must pass the \`formatError\` exported by platform/errors; without it a resolver error reaches the client with its internal message.` });
|
|
55
69
|
}
|
|
56
|
-
return true;
|
|
57
70
|
});
|
|
58
71
|
}
|
|
59
72
|
return { violations, coverage: { status: 'checked', apps, graphqlRegistrations: graphql } };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { treeOf } from './required-files.mjs';
|
|
2
2
|
import { allowsFile } from '../allows.mjs';
|
|
3
3
|
import { isFeTestPath } from '../rules/fe-no-tests.mjs';
|
|
4
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* R94 `fe-slot-allows` (FE_SLOT_FILE_ROLE). A front-end slot that owns a whole directory (`fe.route`, `fe.feature`,
|
|
@@ -10,7 +11,7 @@ import { isFeTestPath } from '../rules/fe-no-tests.mjs';
|
|
|
10
11
|
* root. A file that sits where the slot expects a folder (`hooks/useX.ts`, so the "domain" is a file name) is one too.
|
|
11
12
|
* Files no slot owns are HFS_PATH_NO_SLOT's (`starci app check`), so together every front-end file is placed by exactly one slot and
|
|
12
13
|
* named by it. A test path (a `.spec.tsx` or `.test.tsx` beside a component, a test directory or test tooling) is FE_NO_TESTS's
|
|
13
|
-
* (R97
|
|
14
|
+
* (R97 FE_NO_TESTS): that rule is the one finding of such a file, so this check leaves it alone, as
|
|
14
15
|
* HFS_PATH_NO_SLOT does (scripts/hfs/path-findings.mjs).
|
|
15
16
|
*/
|
|
16
17
|
export const FE_SLOT_ALLOWS_RULE_IDS = ['FE_SLOT_FILE_ROLE'];
|
|
@@ -23,7 +24,7 @@ export function checkFeSlotAllows({ config, graph }) {
|
|
|
23
24
|
const violations = [];
|
|
24
25
|
let files = 0;
|
|
25
26
|
const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
|
|
26
|
-
for (const file of [...tree.files].sort()) {
|
|
27
|
+
for (const file of [...tree.files].sort(byCodeUnit)) {
|
|
27
28
|
if (isFeTestPath(file)) continue; // FE_NO_TESTS's, the one finding of that file
|
|
28
29
|
const classified = resolver.classifyPath(file);
|
|
29
30
|
if (classified.status !== 'owned') continue;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { treeOf } from './required-files.mjs';
|
|
3
3
|
import { allowsFile } from '../allows.mjs';
|
|
4
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* R29 `feature-shape` (BE_FEATURE_SHAPE). A feature root holds `index.ts`, `<feature>.module.ts`, `application/`,
|
|
@@ -14,6 +15,28 @@ export const FEATURE_SHAPE_RULE_IDS = ['BE_FEATURE_SHAPE'];
|
|
|
14
15
|
|
|
15
16
|
const RULE = 'BE_FEATURE_SHAPE';
|
|
16
17
|
|
|
18
|
+
// {message, extra} for one feature file the slot answer refuses, or null when it sits where a slot allows it.
|
|
19
|
+
const featureFileFinding = (file, owner, verdict) => {
|
|
20
|
+
const feature = path.posix.basename(owner.root);
|
|
21
|
+
if (verdict.slot === owner.slot) {
|
|
22
|
+
// A file the feature root slot itself allows is in place (the cli feature root allows its <group>/ folders); any other file
|
|
23
|
+
// deeper than the root sits in a directory no slot claims.
|
|
24
|
+
// A directory entry (`transport/`, `application/`) is held by its own slots: below it, only those slots admit a file.
|
|
25
|
+
if (verdict.allowed && !String(verdict.entry ?? '').endsWith('/')) return null;
|
|
26
|
+
if (verdict.relative.includes('/')) {
|
|
27
|
+
const folder = verdict.relative.split('/')[0];
|
|
28
|
+
return { message: `${file} sits in ${owner.root}/${folder}/, which no slot owns; a feature root holds only ${verdict.allows.join(', ')}, and transport/ holds one folder per protocol a slot names. Move it to application/ or a transport/<protocol>/ folder, or delete it.`,
|
|
29
|
+
extra: { feature, folder } };
|
|
30
|
+
}
|
|
31
|
+
if (verdict.allowed) return null;
|
|
32
|
+
return { message: `${file} is not allowed at the root of feature ${feature}; the root holds only ${verdict.allows.join(', ')}. Move it under application/ or transport/<protocol>/.`, extra: { feature } };
|
|
33
|
+
}
|
|
34
|
+
if (verdict.allowed) return null;
|
|
35
|
+
const where = `${verdict.slot} (${verdict.root})`;
|
|
36
|
+
const forbidden = verdict.forbiddenBy ? ` ${verdict.forbiddenBy} is forbidden there;` : '';
|
|
37
|
+
return { message: `${file} is not allowed in ${where};${forbidden} that folder holds only ${verdict.allows.join(', ')}. Rename it to an allowed role or move it to the folder its role belongs to.`, extra: { feature, slot: verdict.slot } };
|
|
38
|
+
};
|
|
39
|
+
|
|
17
40
|
export function checkFeatureShape({ config, graph }) {
|
|
18
41
|
const resolver = graph.resolver;
|
|
19
42
|
const tree = treeOf(config.root);
|
|
@@ -21,32 +44,15 @@ export function checkFeatureShape({ config, graph }) {
|
|
|
21
44
|
const features = new Set();
|
|
22
45
|
let files = 0;
|
|
23
46
|
const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
|
|
24
|
-
for (const file of [...tree.files].sort()) {
|
|
47
|
+
for (const file of [...tree.files].sort(byCodeUnit)) {
|
|
25
48
|
const owner = resolver.ownerOf(file);
|
|
26
49
|
if (!owner || resolver.slot(owner.slot)?.tier !== 'feature') continue;
|
|
27
50
|
features.add(owner.root);
|
|
28
51
|
files += 1;
|
|
29
52
|
const verdict = allowsFile(resolver, file);
|
|
30
53
|
if (!verdict) continue;
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (verdict.slot === owner.slot) {
|
|
34
|
-
// A file the feature root slot itself allows is in place (the cli feature root allows its <group>/ folders); any other file
|
|
35
|
-
// deeper than the root sits in a directory no slot claims.
|
|
36
|
-
// A directory entry (`transport/`, `application/`) is held by its own slots: below it, only those slots admit a file.
|
|
37
|
-
if (verdict.allowed && !String(verdict.entry ?? '').endsWith('/')) continue;
|
|
38
|
-
if (verdict.relative.includes('/')) {
|
|
39
|
-
const folder = verdict.relative.split('/')[0];
|
|
40
|
-
report(file, `${file} sits in ${owner.root}/${folder}/, which no slot owns; a feature root holds only ${verdict.allows.join(', ')}, and transport/ holds one folder per protocol a slot names. Move it to application/ or a transport/<protocol>/ folder, or delete it.`,
|
|
41
|
-
{ feature, folder });
|
|
42
|
-
} else if (!verdict.allowed) {
|
|
43
|
-
report(file, `${file} is not allowed at the root of feature ${feature}; the root holds only ${verdict.allows.join(', ')}. Move it under application/ or transport/<protocol>/.`, { feature });
|
|
44
|
-
}
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (verdict.allowed) continue;
|
|
48
|
-
const forbidden = verdict.forbiddenBy ? ` ${verdict.forbiddenBy} is forbidden there;` : '';
|
|
49
|
-
report(file, `${file} is not allowed in ${where};${forbidden} that folder holds only ${verdict.allows.join(', ')}. Rename it to an allowed role or move it to the folder its role belongs to.`, { feature, slot: verdict.slot });
|
|
54
|
+
const finding = featureFileFinding(file, owner, verdict);
|
|
55
|
+
if (finding) report(file, finding.message, finding.extra);
|
|
50
56
|
}
|
|
51
57
|
return { violations, coverage: { status: 'checked', features: features.size, files } };
|
|
52
58
|
}
|
|
@@ -15,7 +15,7 @@ const cache = new Map(); // only the installed knowledge file is cached; an expl
|
|
|
15
15
|
const EXACT_NAME = /^[^\\/*?[\]{}\s]+$/;
|
|
16
16
|
|
|
17
17
|
function unavailable(file, detail) {
|
|
18
|
-
return Error(`ARCH_KNOWLEDGE_UNAVAILABLE: ${file} ${detail}`);
|
|
18
|
+
return new Error(`ARCH_KNOWLEDGE_UNAVAILABLE: ${file} ${detail}`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function load(file) {
|