@agjs/tsforge 0.1.19 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +6 -2
  2. package/scripts/browser-check.ts +41 -5
  3. package/scripts/build-rules-md.ts +78 -21
  4. package/scripts/cli-metrics.ts +10 -0
  5. package/scripts/sweep.ts +53 -23
  6. package/scripts/web-sweep.ts +292 -0
  7. package/src/browser/index.ts +3 -0
  8. package/src/browser/oracle.ts +215 -8
  9. package/src/cli.ts +22 -4
  10. package/src/config/index.ts +8 -0
  11. package/src/config/profiles.ts +150 -0
  12. package/src/config/tsforge-config.ts +64 -5
  13. package/src/detect-gate.ts +144 -13
  14. package/src/eval/eval.types.ts +9 -0
  15. package/src/eval/failure-class.ts +263 -0
  16. package/src/eval/index.ts +8 -0
  17. package/src/eval/metrics.ts +7 -0
  18. package/src/eval/parse-log.ts +105 -0
  19. package/src/eval/report.ts +19 -0
  20. package/src/eval/score.ts +10 -0
  21. package/src/loop/feedback/meta-rule-docs.ts +48 -0
  22. package/src/loop/feedback/rule-docs.ts +150 -0
  23. package/src/loop/loop.types.ts +4 -0
  24. package/src/loop/rule-docs.generated.json +131 -1
  25. package/src/loop/ttsr-defaults.ts +175 -4
  26. package/src/loop/turn.ts +3 -0
  27. package/src/meta-rules/registry.ts +32 -0
  28. package/src/meta-rules/rules/ci/no-github-context-in-shell.ts +40 -0
  29. package/src/meta-rules/rules/ci/no-pull-request-target-untrusted-checkout.ts +42 -0
  30. package/src/meta-rules/rules/ci/workflow-permissions-explicit.ts +49 -0
  31. package/src/meta-rules/rules/ci/workflow-permissions-least-privilege.ts +44 -0
  32. package/src/meta-rules/rules/config/next-image-remote-patterns-no-wildcards.ts +77 -0
  33. package/src/meta-rules/rules/config/next-instrumentation-present.ts +66 -0
  34. package/src/meta-rules/rules/config/next-proxy-over-middleware.ts +64 -0
  35. package/src/meta-rules/rules/config/tsconfig-recommended-flags.ts +75 -0
  36. package/src/meta-rules/rules/supply-chain/dependency-overrides-require-comment.ts +61 -0
  37. package/src/meta-rules/rules/supply-chain/fastify-security-plugins.ts +54 -0
  38. package/src/meta-rules/rules/supply-chain/lockfile-required.ts +51 -0
  39. package/src/meta-rules/rules/supply-chain/migrations-must-be-checked-in.ts +49 -0
  40. package/src/meta-rules/rules/supply-chain/no-git-or-tarball-dependencies.ts +70 -0
  41. package/src/meta-rules/rules/supply-chain/package-manager-field-required.ts +31 -0
  42. package/src/meta-rules/rules/supply-chain/production-must-not-use-drizzle-push.ts +75 -0
  43. package/src/meta-rules/rules/supply-chain/single-package-manager.ts +30 -0
  44. package/src/meta-rules/utils/lockfiles.ts +105 -0
  45. package/src/meta-rules/utils/workflow-yaml.ts +86 -0
  46. package/src/rule-packs/authorization/index.ts +26 -0
  47. package/src/rule-packs/authorization/rules/id-param-requires-object-authz.ts +87 -0
  48. package/src/rule-packs/authorization/rules/mutating-route-requires-authz.ts +116 -0
  49. package/src/rule-packs/authorization/rules/server-action-requires-authz.ts +101 -0
  50. package/src/rule-packs/authorization/utils.ts +285 -0
  51. package/src/rule-packs/boundary-utils.ts +13 -0
  52. package/src/rule-packs/code-flow/index.ts +4 -1
  53. package/src/rule-packs/code-flow/rules/no-throw-literal.ts +67 -0
  54. package/src/rule-packs/drizzle/index.ts +7 -0
  55. package/src/rule-packs/drizzle/rules/update-delete-account-scoped-must-filter-scope.ts +106 -0
  56. package/src/rule-packs/drizzle/rules/update-delete-must-have-where.ts +73 -0
  57. package/src/rule-packs/drizzle/utils.ts +133 -1
  58. package/src/rule-packs/fastify/index.ts +38 -0
  59. package/src/rule-packs/fastify/rules/error-handler-must-set-status.ts +78 -0
  60. package/src/rule-packs/fastify/rules/prefer-return-over-reply-send.ts +104 -0
  61. package/src/rule-packs/fastify/rules/require-fp-for-shared-plugins.ts +106 -0
  62. package/src/rule-packs/fastify/rules/require-plugin-name.ts +54 -0
  63. package/src/rule-packs/fastify/rules/require-response-schema.ts +62 -0
  64. package/src/rule-packs/fastify/rules/require-route-schema.ts +104 -0
  65. package/src/rule-packs/fastify/rules/test-inject-must-close-app.ts +44 -0
  66. package/src/rule-packs/fastify/utils/fastifyChain.ts +231 -0
  67. package/src/rule-packs/index.ts +10 -0
  68. package/src/rule-packs/jwt-cookies/index.ts +10 -0
  69. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-maxage-or-expires.ts +132 -0
  70. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-samesite.ts +151 -0
  71. package/src/rule-packs/jwt-cookies/rules/jwt-must-verify-not-decode.ts +124 -0
  72. package/src/rule-packs/module-boundaries/index.ts +3 -0
  73. package/src/rule-packs/module-boundaries/rules/no-react-in-services.ts +111 -0
  74. package/src/rule-packs/nextjs/index.ts +32 -0
  75. package/src/rule-packs/nextjs/rules/await-dynamic-request-apis.ts +65 -0
  76. package/src/rule-packs/nextjs/rules/error-boundary-require-use-client.ts +38 -0
  77. package/src/rule-packs/nextjs/rules/mutation-should-revalidate-cache.ts +152 -0
  78. package/src/rule-packs/nextjs/rules/no-html-img-element.ts +45 -0
  79. package/src/rule-packs/nextjs/rules/no-internal-api-fetch.ts +126 -0
  80. package/src/rule-packs/nextjs/rules/no-secret-props-to-client.ts +118 -0
  81. package/src/rule-packs/nextjs/rules/no-sensitive-next-public-env.ts +72 -0
  82. package/src/rule-packs/nextjs/rules/prefer-lazy-use-state-init.ts +85 -0
  83. package/src/rule-packs/nextjs/rules/server-action-requires-authz-and-validation.ts +178 -0
  84. package/src/rule-packs/nextjs/rules/server-only-modules-import-server-only.ts +87 -0
  85. package/src/rule-packs/nextjs/utils.ts +18 -0
  86. package/src/rule-packs/react-component-architecture/index.ts +18 -0
  87. package/src/rule-packs/react-component-architecture/rules/dangerous-html-requires-sanitize.ts +83 -0
  88. package/src/rule-packs/react-component-architecture/rules/no-anonymous-useEffect.ts +61 -0
  89. package/src/rule-packs/react-component-architecture/rules/no-component-invocation.ts +55 -0
  90. package/src/rule-packs/react-component-architecture/rules/no-derived-state-in-effect.ts +204 -0
  91. package/src/rule-packs/react-component-architecture/rules/no-nested-component.ts +152 -0
  92. package/src/rule-packs/react-component-architecture/rules/no-react-fc.ts +57 -0
  93. package/src/rule-packs/rule-catalog.types.ts +21 -0
  94. package/src/rule-packs/rule-metadata.ts +163 -0
  95. package/src/rule-packs/runtime-boundaries/index.ts +33 -0
  96. package/src/rule-packs/runtime-boundaries/rules/no-prototype-polluting-merge.ts +113 -0
  97. package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-fetch-url.ts +69 -0
  98. package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-redirect.ts +79 -0
  99. package/src/rule-packs/runtime-boundaries/rules/upload-must-set-limits.ts +126 -0
  100. package/src/rule-packs/runtime-boundaries/rules/webhook-must-verify-signature-before-parse.ts +87 -0
  101. package/src/rule-packs/security/index.ts +35 -0
  102. package/src/rule-packs/security/rules/catch-must-handle.ts +126 -0
  103. package/src/rule-packs/security/rules/no-auth-token-in-storage.ts +107 -0
  104. package/src/rule-packs/security/rules/no-child-process-exec.ts +72 -0
  105. package/src/rule-packs/security/rules/no-dynamic-regexp.ts +56 -0
  106. package/src/rule-packs/security/rules/no-inner-html-assignment.ts +42 -0
  107. package/src/rule-packs/security/rules/no-spawn-with-shell.ts +106 -0
  108. package/src/rule-packs/structured-logging/index.ts +6 -0
  109. package/src/rule-packs/structured-logging/rules/caught-error-log-requires-cause.ts +234 -0
  110. package/src/rule-packs/structured-logging/rules/logger-not-console.ts +146 -0
  111. package/src/rule-packs/test-conventions/index.ts +9 -0
  112. package/src/rule-packs/test-conventions/rules/fake-timers-must-be-restored.ts +143 -0
  113. package/src/rule-packs/test-conventions/rules/no-conditional-expect.ts +77 -0
  114. package/src/rule-packs/test-conventions/rules/no-real-network-in-unit-tests.ts +174 -0
  115. package/src/rule-packs/typescript-core/index.ts +30 -0
  116. package/src/rule-packs/typescript-core/rules/exported-functions-require-return-type.ts +74 -0
  117. package/src/rule-packs/typescript-core/rules/fetch-must-check-ok.ts +106 -0
  118. package/src/rule-packs/typescript-core/rules/json-parse-must-validate.ts +97 -0
  119. package/src/rule-packs/typescript-core/rules/no-unsafe-boundary-cast.ts +70 -0
  120. package/src/stack-detection/packs.ts +57 -0
  121. package/strict.type-aware.eslint.config.mjs +33 -0
  122. package/strict.web.eslint.config.mjs +32 -1
@@ -0,0 +1,174 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import { toPosixRelative } from "../../utils";
6
+
7
+ export const RULE_NAME = "no-real-network-in-unit-tests";
8
+
9
+ export interface INoRealNetworkInUnitTestsOptions {
10
+ readonly testFileSuffixes?: readonly string[];
11
+ readonly integrationMarkers?: readonly string[];
12
+ readonly networkCallees?: readonly string[];
13
+ }
14
+
15
+ type RuleOptions = [INoRealNetworkInUnitTestsOptions];
16
+ type MessageIds = "realNetworkInUnitTest";
17
+
18
+ const DEFAULT_TEST_FILE_SUFFIXES = [
19
+ ".test.ts",
20
+ ".test.tsx",
21
+ ".spec.ts",
22
+ ".spec.tsx",
23
+ ] as const;
24
+
25
+ const DEFAULT_INTEGRATION_MARKERS = [
26
+ ".integration.test.",
27
+ ".integration.spec.",
28
+ "/integration/",
29
+ ] as const;
30
+
31
+ const DEFAULT_NETWORK_CALLEES = ["fetch"] as const;
32
+
33
+ const optionSchema: JSONSchema4 = {
34
+ type: "object",
35
+ additionalProperties: false,
36
+ properties: {
37
+ testFileSuffixes: {
38
+ type: "array",
39
+ items: { type: "string" },
40
+ },
41
+ integrationMarkers: {
42
+ type: "array",
43
+ items: { type: "string" },
44
+ },
45
+ networkCallees: {
46
+ type: "array",
47
+ items: { type: "string" },
48
+ },
49
+ },
50
+ };
51
+
52
+ function isUnitTestFile(relPath: string, suffixes: readonly string[]): boolean {
53
+ return suffixes.some((suffix) => relPath.endsWith(suffix));
54
+ }
55
+
56
+ function isIntegrationTestFile(
57
+ relPath: string,
58
+ markers: readonly string[]
59
+ ): boolean {
60
+ return markers.some((marker) => relPath.includes(marker));
61
+ }
62
+
63
+ function getCalleeName(callee: TSESTree.Node): string | null {
64
+ if (callee.type === AST_NODE_TYPES.Identifier) {
65
+ return callee.name;
66
+ }
67
+
68
+ if (
69
+ callee.type === AST_NODE_TYPES.MemberExpression &&
70
+ !callee.computed &&
71
+ callee.property.type === AST_NODE_TYPES.Identifier
72
+ ) {
73
+ return callee.property.name;
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ function isAxiosCall(node: TSESTree.CallExpression): boolean {
80
+ const callee = node.callee;
81
+
82
+ if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
83
+ return false;
84
+ }
85
+
86
+ if (
87
+ callee.object.type !== AST_NODE_TYPES.Identifier ||
88
+ callee.object.name !== "axios"
89
+ ) {
90
+ return false;
91
+ }
92
+
93
+ if (callee.property.type !== AST_NODE_TYPES.Identifier) {
94
+ return false;
95
+ }
96
+
97
+ const method = callee.property.name;
98
+
99
+ return (
100
+ method === "get" ||
101
+ method === "post" ||
102
+ method === "put" ||
103
+ method === "patch" ||
104
+ method === "delete" ||
105
+ method === "request"
106
+ );
107
+ }
108
+
109
+ export const noRealNetworkInUnitTestsRule = createRule<RuleOptions, MessageIds>(
110
+ {
111
+ name: RULE_NAME,
112
+ meta: {
113
+ type: "suggestion",
114
+ docs: {
115
+ description:
116
+ "Unit tests should not perform real network I/O — mock HTTP clients or move the test to an integration suite.",
117
+ },
118
+ schema: [optionSchema],
119
+ messages: {
120
+ realNetworkInUnitTest:
121
+ "Avoid real network calls in unit tests — mock `{{callee}}` or move this test to an integration file.",
122
+ },
123
+ },
124
+ defaultOptions: [
125
+ {
126
+ testFileSuffixes: [...DEFAULT_TEST_FILE_SUFFIXES],
127
+ integrationMarkers: [...DEFAULT_INTEGRATION_MARKERS],
128
+ networkCallees: [...DEFAULT_NETWORK_CALLEES],
129
+ },
130
+ ],
131
+ create(context, [options]) {
132
+ const testSuffixes =
133
+ options.testFileSuffixes ?? DEFAULT_TEST_FILE_SUFFIXES;
134
+ const integrationMarkers =
135
+ options.integrationMarkers ?? DEFAULT_INTEGRATION_MARKERS;
136
+ const networkCallees = new Set(
137
+ options.networkCallees ?? DEFAULT_NETWORK_CALLEES
138
+ );
139
+ const relPath = toPosixRelative(context.filename, context.cwd);
140
+
141
+ if (!isUnitTestFile(relPath, testSuffixes)) {
142
+ return {};
143
+ }
144
+
145
+ if (isIntegrationTestFile(relPath, integrationMarkers)) {
146
+ return {};
147
+ }
148
+
149
+ return {
150
+ CallExpression(node) {
151
+ const name = getCalleeName(node.callee);
152
+
153
+ if (name !== null && networkCallees.has(name)) {
154
+ context.report({
155
+ node,
156
+ messageId: "realNetworkInUnitTest",
157
+ data: { callee: name },
158
+ });
159
+
160
+ return;
161
+ }
162
+
163
+ if (isAxiosCall(node)) {
164
+ context.report({
165
+ node,
166
+ messageId: "realNetworkInUnitTest",
167
+ data: { callee: "axios" },
168
+ });
169
+ }
170
+ },
171
+ };
172
+ },
173
+ }
174
+ );
@@ -0,0 +1,30 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+
3
+ import { exportedFunctionsRequireReturnTypeRule } from "./rules/exported-functions-require-return-type";
4
+ import { fetchMustCheckOkRule } from "./rules/fetch-must-check-ok";
5
+ import { jsonParseMustValidateRule } from "./rules/json-parse-must-validate";
6
+ import { noUnsafeBoundaryCastRule } from "./rules/no-unsafe-boundary-cast";
7
+ import type { IRulePack } from "../rule-packs.types";
8
+
9
+ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
10
+ "exported-functions-require-return-type":
11
+ exportedFunctionsRequireReturnTypeRule,
12
+ "fetch-must-check-ok": fetchMustCheckOkRule,
13
+ "json-parse-must-validate": jsonParseMustValidateRule,
14
+ "no-unsafe-boundary-cast": noUnsafeBoundaryCastRule,
15
+ };
16
+
17
+ export const typescriptCorePack: IRulePack = {
18
+ id: "typescript-core",
19
+ description:
20
+ "Cross-cutting TypeScript boundary safety: fetch status checks, JSON validation, and module export typing.",
21
+ rules,
22
+ rulesConfig: {
23
+ "exported-functions-require-return-type": "warn",
24
+ "fetch-must-check-ok": "error",
25
+ "json-parse-must-validate": "error",
26
+ "no-unsafe-boundary-cast": "error",
27
+ },
28
+ };
29
+
30
+ export default typescriptCorePack;
@@ -0,0 +1,74 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "exported-functions-require-return-type";
6
+
7
+ type MessageIds = "missingReturnType";
8
+
9
+ function isExported(node: TSESTree.Node): boolean {
10
+ return (
11
+ node.type === AST_NODE_TYPES.ExportNamedDeclaration ||
12
+ node.type === AST_NODE_TYPES.ExportDefaultDeclaration
13
+ );
14
+ }
15
+
16
+ export const exportedFunctionsRequireReturnTypeRule = createRule<
17
+ [],
18
+ MessageIds
19
+ >({
20
+ name: RULE_NAME,
21
+ meta: {
22
+ type: "suggestion",
23
+ docs: {
24
+ description:
25
+ "Exported functions should declare an explicit return type at module boundaries.",
26
+ },
27
+ schema: [],
28
+ messages: {
29
+ missingReturnType:
30
+ "Exported function `{{name}}` should declare an explicit return type.",
31
+ },
32
+ },
33
+ defaultOptions: [],
34
+ create(context) {
35
+ function checkFunction(
36
+ node: TSESTree.FunctionDeclaration,
37
+ exported: boolean
38
+ ): void {
39
+ if (!exported || node.returnType !== undefined) {
40
+ return;
41
+ }
42
+
43
+ const name = node.id?.name ?? "anonymous";
44
+
45
+ context.report({
46
+ node,
47
+ messageId: "missingReturnType",
48
+ data: { name },
49
+ });
50
+ }
51
+
52
+ return {
53
+ ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration) {
54
+ const decl = node.declaration;
55
+
56
+ if (decl?.type === AST_NODE_TYPES.FunctionDeclaration) {
57
+ checkFunction(decl, true);
58
+ }
59
+ },
60
+ FunctionDeclaration(node: TSESTree.FunctionDeclaration) {
61
+ if (node.parent !== undefined && isExported(node.parent)) {
62
+ return;
63
+ }
64
+
65
+ if (
66
+ node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration ||
67
+ node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration
68
+ ) {
69
+ checkFunction(node, true);
70
+ }
71
+ },
72
+ };
73
+ },
74
+ });
@@ -0,0 +1,106 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+ import { walkSome } from "../../utils";
5
+
6
+ export const RULE_NAME = "fetch-must-check-ok";
7
+
8
+ type MessageIds = "missingOkCheck";
9
+
10
+ function fetchCallHasOkCheck(fetchCall: TSESTree.CallExpression): boolean {
11
+ let parent: TSESTree.Node | null | undefined = fetchCall.parent;
12
+
13
+ while (parent !== undefined && parent !== null) {
14
+ if (parent.type === AST_NODE_TYPES.AwaitExpression) {
15
+ parent = parent.parent;
16
+ continue;
17
+ }
18
+
19
+ if (parent.type === AST_NODE_TYPES.VariableDeclarator) {
20
+ const init = parent.init;
21
+
22
+ if (
23
+ init?.type === AST_NODE_TYPES.AwaitExpression &&
24
+ init.argument === fetchCall
25
+ ) {
26
+ const binding = parent.id;
27
+
28
+ if (binding.type === AST_NODE_TYPES.Identifier) {
29
+ const name = binding.name;
30
+
31
+ return walkSome(parent.parent ?? fetchCall, (node) => {
32
+ if (
33
+ node.type !== AST_NODE_TYPES.MemberExpression ||
34
+ node.computed
35
+ ) {
36
+ return false;
37
+ }
38
+
39
+ return (
40
+ node.object.type === AST_NODE_TYPES.Identifier &&
41
+ node.object.name === name &&
42
+ node.property.type === AST_NODE_TYPES.Identifier &&
43
+ node.property.name === "ok"
44
+ );
45
+ });
46
+ }
47
+ }
48
+ }
49
+
50
+ if (
51
+ parent.type === AST_NODE_TYPES.MemberExpression &&
52
+ !parent.computed &&
53
+ parent.property.type === AST_NODE_TYPES.Identifier &&
54
+ parent.property.name === "json"
55
+ ) {
56
+ return false;
57
+ }
58
+
59
+ break;
60
+ }
61
+
62
+ return true;
63
+ }
64
+
65
+ export const fetchMustCheckOkRule = createRule<[], MessageIds>({
66
+ name: RULE_NAME,
67
+ meta: {
68
+ type: "problem",
69
+ docs: {
70
+ description:
71
+ "HTTP fetch responses must check `.ok` or status before calling `.json()`.",
72
+ },
73
+ schema: [],
74
+ messages: {
75
+ missingOkCheck:
76
+ "Check `response.ok` (or status) before calling `.json()` on a fetch response.",
77
+ },
78
+ },
79
+ defaultOptions: [],
80
+ create(context) {
81
+ return {
82
+ CallExpression(node: TSESTree.CallExpression) {
83
+ const callee = node.callee;
84
+
85
+ if (
86
+ callee.type !== AST_NODE_TYPES.Identifier ||
87
+ callee.name !== "fetch"
88
+ ) {
89
+ return;
90
+ }
91
+
92
+ const parent = node.parent;
93
+
94
+ if (
95
+ parent?.type === AST_NODE_TYPES.MemberExpression &&
96
+ !parent.computed &&
97
+ parent.property.type === AST_NODE_TYPES.Identifier &&
98
+ parent.property.name === "json" &&
99
+ !fetchCallHasOkCheck(node)
100
+ ) {
101
+ context.report({ node: parent, messageId: "missingOkCheck" });
102
+ }
103
+ },
104
+ };
105
+ },
106
+ });
@@ -0,0 +1,97 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "json-parse-must-validate";
6
+
7
+ type MessageIds = "bareJsonParse";
8
+
9
+ const VALIDATOR_IMPORTS = new Set([
10
+ "zod",
11
+ "valibot",
12
+ "@effect/schema",
13
+ "effect/Schema",
14
+ "arktype",
15
+ ]);
16
+
17
+ function fileHasValidatorImport(program: TSESTree.Program): boolean {
18
+ for (const stmt of program.body) {
19
+ if (stmt.type !== AST_NODE_TYPES.ImportDeclaration) {
20
+ continue;
21
+ }
22
+
23
+ const source = stmt.source.value;
24
+
25
+ if (typeof source !== "string") {
26
+ continue;
27
+ }
28
+
29
+ const base = source.split("/")[0];
30
+
31
+ if (base !== undefined && VALIDATOR_IMPORTS.has(base)) {
32
+ return true;
33
+ }
34
+
35
+ if (VALIDATOR_IMPORTS.has(source)) {
36
+ return true;
37
+ }
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ function isTestFile(filename: string): boolean {
44
+ return (
45
+ filename.includes(".test.") ||
46
+ filename.includes(".spec.") ||
47
+ filename.includes("/__tests__/")
48
+ );
49
+ }
50
+
51
+ export const jsonParseMustValidateRule = createRule<[], MessageIds>({
52
+ name: RULE_NAME,
53
+ meta: {
54
+ type: "problem",
55
+ docs: {
56
+ description:
57
+ "Disallow bare JSON.parse on untrusted input — validate through a schema library.",
58
+ },
59
+ schema: [],
60
+ messages: {
61
+ bareJsonParse:
62
+ "Do not use bare `JSON.parse` on external input — parse through Zod, Valibot, or Effect Schema.",
63
+ },
64
+ },
65
+ defaultOptions: [],
66
+ create(context) {
67
+ if (isTestFile(context.filename)) {
68
+ return {};
69
+ }
70
+
71
+ let hasValidator = false;
72
+
73
+ return {
74
+ Program(node: TSESTree.Program) {
75
+ hasValidator = fileHasValidatorImport(node);
76
+ },
77
+ CallExpression(node: TSESTree.CallExpression) {
78
+ if (hasValidator) {
79
+ return;
80
+ }
81
+
82
+ const callee = node.callee;
83
+
84
+ if (
85
+ callee.type === AST_NODE_TYPES.MemberExpression &&
86
+ !callee.computed &&
87
+ callee.object.type === AST_NODE_TYPES.Identifier &&
88
+ callee.object.name === "JSON" &&
89
+ callee.property.type === AST_NODE_TYPES.Identifier &&
90
+ callee.property.name === "parse"
91
+ ) {
92
+ context.report({ node, messageId: "bareJsonParse" });
93
+ }
94
+ },
95
+ };
96
+ },
97
+ });
@@ -0,0 +1,70 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-unsafe-boundary-cast";
6
+
7
+ type MessageIds = "unsafeBoundaryCast";
8
+
9
+ const BOUNDARY_CALLEES = new Set([
10
+ "parse",
11
+ "json",
12
+ "get",
13
+ "getAll",
14
+ "text",
15
+ "formData",
16
+ ]);
17
+
18
+ function isBoundarySource(node: TSESTree.Node): boolean {
19
+ if (node.type === AST_NODE_TYPES.CallExpression) {
20
+ const callee = node.callee;
21
+
22
+ if (
23
+ callee.type === AST_NODE_TYPES.MemberExpression &&
24
+ !callee.computed &&
25
+ callee.property.type === AST_NODE_TYPES.Identifier &&
26
+ BOUNDARY_CALLEES.has(callee.property.name)
27
+ ) {
28
+ return true;
29
+ }
30
+
31
+ if (
32
+ callee.type === AST_NODE_TYPES.MemberExpression &&
33
+ !callee.computed &&
34
+ callee.object.type === AST_NODE_TYPES.Identifier &&
35
+ callee.object.name === "JSON" &&
36
+ callee.property.type === AST_NODE_TYPES.Identifier &&
37
+ callee.property.name === "parse"
38
+ ) {
39
+ return true;
40
+ }
41
+ }
42
+
43
+ return false;
44
+ }
45
+
46
+ export const noUnsafeBoundaryCastRule = createRule<[], MessageIds>({
47
+ name: RULE_NAME,
48
+ meta: {
49
+ type: "problem",
50
+ docs: {
51
+ description:
52
+ "Disallow type assertions immediately after parsing untrusted boundary input.",
53
+ },
54
+ schema: [],
55
+ messages: {
56
+ unsafeBoundaryCast:
57
+ "Do not cast untrusted parsed input with `as` — validate with a runtime schema instead.",
58
+ },
59
+ },
60
+ defaultOptions: [],
61
+ create(context) {
62
+ return {
63
+ TSAsExpression(node: TSESTree.TSAsExpression) {
64
+ if (isBoundarySource(node.expression)) {
65
+ context.report({ node, messageId: "unsafeBoundaryCast" });
66
+ }
67
+ },
68
+ };
69
+ },
70
+ });
@@ -61,6 +61,17 @@ export const PACK_REGISTRY = {
61
61
  guidance: "Follow Elysia patterns for HTTP routing and middleware.",
62
62
  } as const satisfies IRulePackDescriptor,
63
63
 
64
+ fastify: {
65
+ id: "fastify",
66
+ label: "Fastify",
67
+ description:
68
+ "Schema-first Fastify routing, plugin encapsulation, and test hygiene",
69
+ category: "framework",
70
+ appliesWhen: { anyDeps: ["fastify"] },
71
+ guidance:
72
+ "Use schema-driven routes, fastify-plugin for shared decorators, and fastify.inject with app.close() in tests.",
73
+ } as const satisfies IRulePackDescriptor,
74
+
64
75
  nextjs: {
65
76
  id: "nextjs",
66
77
  label: "Next.js",
@@ -132,6 +143,50 @@ export const PACK_REGISTRY = {
132
143
  guidance: "Write comments that explain intent, not what the code does.",
133
144
  } as const satisfies IRulePackDescriptor,
134
145
 
146
+ security: {
147
+ id: "security",
148
+ label: "Security",
149
+ description:
150
+ "Application security guardrails: command injection, ReDoS, DOM XSS, and silent error masking",
151
+ category: "infra",
152
+ appliesWhen: { always: true },
153
+ guidance:
154
+ "Avoid shell execution, dynamic regex, innerHTML assignment, and catch blocks that silently mask failures.",
155
+ } as const satisfies IRulePackDescriptor,
156
+
157
+ "runtime-boundaries": {
158
+ id: "runtime-boundaries",
159
+ label: "Runtime Boundaries",
160
+ description:
161
+ "Runtime boundary safety: open redirects, SSRF, prototype pollution, webhook verification, and upload limits",
162
+ category: "infra",
163
+ appliesWhen: { always: true },
164
+ guidance:
165
+ "Use literal redirect/fetch URLs, avoid merging request fields into objects, verify webhooks before parsing, and cap multipart uploads.",
166
+ } as const satisfies IRulePackDescriptor,
167
+
168
+ "typescript-core": {
169
+ id: "typescript-core",
170
+ label: "TypeScript Core",
171
+ description:
172
+ "Cross-cutting TypeScript boundary safety: fetch status checks, JSON validation, and export typing",
173
+ category: "language",
174
+ appliesWhen: { always: false },
175
+ guidance:
176
+ "Validate HTTP responses, parse JSON through schemas, and annotate exported function return types.",
177
+ } as const satisfies IRulePackDescriptor,
178
+
179
+ authorization: {
180
+ id: "authorization",
181
+ label: "Authorization",
182
+ description:
183
+ "Experimental authorization heuristics for routes, server actions, and object-level access",
184
+ category: "infra",
185
+ appliesWhen: { always: false },
186
+ guidance:
187
+ "Mutating handlers should call authorization helpers before writes; opt in via the security profile.",
188
+ } as const satisfies IRulePackDescriptor,
189
+
135
190
  "structured-logging": {
136
191
  id: "structured-logging",
137
192
  label: "Structured Logging",
@@ -176,6 +231,8 @@ export const ALWAYS_ON_PACKS = [
176
231
  "module-boundaries",
177
232
  "code-flow",
178
233
  "comment-hygiene",
234
+ "security",
235
+ "runtime-boundaries",
179
236
  ] as const;
180
237
 
181
238
  /** Type-safe record of all pack descriptors. */
@@ -0,0 +1,33 @@
1
+ // Optional type-aware ESLint overlay — enabled only when the target has a
2
+ // compiling tsconfig (see detect-gate.ts). Adds async correctness rules that
3
+ // require parserOptions.project; kept separate from strict.eslint.config.mjs
4
+ // so the syntactic gate still runs on any .ts file without type info.
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["**/node_modules/**", "**/dist/**", "**/build/**"] },
9
+ {
10
+ files: ["**/*.ts", "**/*.tsx"],
11
+ languageOptions: {
12
+ parser: tseslint.parser,
13
+ parserOptions: {
14
+ projectService: true,
15
+ tsconfigRootDir: process.cwd(),
16
+ },
17
+ },
18
+ plugins: {
19
+ "@typescript-eslint": tseslint.plugin,
20
+ },
21
+ rules: {
22
+ "@typescript-eslint/no-floating-promises": "error",
23
+ "@typescript-eslint/no-misused-promises": [
24
+ "error",
25
+ {
26
+ checksVoidReturn: {
27
+ attributes: false,
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ }
33
+ );