@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,44 @@
1
+ import type { TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+ import { isTestFile } from "../../react-component-architecture/utils";
5
+ import { nodeContainsMemberCall } from "../utils/fastifyChain";
6
+
7
+ export const RULE_NAME = "test-inject-must-close-app";
8
+
9
+ type MessageIds = "missingAppClose";
10
+
11
+ export const testInjectMustCloseAppRule = createRule<[], MessageIds>({
12
+ name: RULE_NAME,
13
+ meta: {
14
+ type: "problem",
15
+ docs: {
16
+ description:
17
+ "Test files using fastify.inject must register teardown that calls app.close() to drain connections.",
18
+ },
19
+ schema: [],
20
+ messages: {
21
+ missingAppClose:
22
+ "Tests using `.inject(...)` must call `app.close()` in an `after`/`t.after` hook to drain connection pools.",
23
+ },
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ const filename = context.filename;
28
+
29
+ if (!isTestFile(filename)) {
30
+ return {};
31
+ }
32
+
33
+ return {
34
+ "Program:exit"(program: TSESTree.Program) {
35
+ const usesInject = nodeContainsMemberCall(program, "inject");
36
+ const closesApp = nodeContainsMemberCall(program, "close");
37
+
38
+ if (usesInject && !closesApp) {
39
+ context.report({ node: program, messageId: "missingAppClose" });
40
+ }
41
+ },
42
+ };
43
+ },
44
+ });
@@ -0,0 +1,231 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { walkSome } from "../../utils";
4
+
5
+ export const ROUTE_METHODS = new Set([
6
+ "get",
7
+ "post",
8
+ "put",
9
+ "patch",
10
+ "delete",
11
+ "head",
12
+ "options",
13
+ "all",
14
+ ]);
15
+
16
+ export const MUTATING_METHODS = new Set(["post", "put", "patch"]);
17
+
18
+ /** Collect identifiers bound to a Fastify instance (`Fastify()` / `require('fastify')()`). */
19
+ export function collectFastifyVariables(
20
+ program: TSESTree.Program
21
+ ): Set<string> {
22
+ const vars = new Set<string>();
23
+
24
+ for (const statement of program.body) {
25
+ if (statement.type !== AST_NODE_TYPES.VariableDeclaration) {
26
+ continue;
27
+ }
28
+
29
+ for (const decl of statement.declarations) {
30
+ if (decl.id.type !== AST_NODE_TYPES.Identifier || decl.init === null) {
31
+ continue;
32
+ }
33
+
34
+ if (isFastifyFactoryCall(decl.init)) {
35
+ vars.add(decl.id.name);
36
+ }
37
+ }
38
+ }
39
+
40
+ return vars;
41
+ }
42
+
43
+ function isFastifyFactoryCall(node: TSESTree.Expression): boolean {
44
+ if (node.type !== AST_NODE_TYPES.CallExpression) {
45
+ return false;
46
+ }
47
+
48
+ const callee = node.callee;
49
+
50
+ if (callee.type === AST_NODE_TYPES.CallExpression) {
51
+ return isFastifyFactoryCall(callee);
52
+ }
53
+
54
+ if (callee.type === AST_NODE_TYPES.Identifier && callee.name === "Fastify") {
55
+ return true;
56
+ }
57
+
58
+ if (
59
+ callee.type === AST_NODE_TYPES.MemberExpression &&
60
+ !callee.computed &&
61
+ callee.property.type === AST_NODE_TYPES.Identifier &&
62
+ callee.property.name === "default" &&
63
+ callee.object.type === AST_NODE_TYPES.Identifier &&
64
+ callee.object.name === "Fastify"
65
+ ) {
66
+ return true;
67
+ }
68
+
69
+ return false;
70
+ }
71
+
72
+ export function getRouteMethodName(
73
+ node: TSESTree.CallExpression,
74
+ fastifyVars: ReadonlySet<string>
75
+ ): string | null {
76
+ const callee = node.callee;
77
+
78
+ if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
79
+ return null;
80
+ }
81
+
82
+ if (
83
+ callee.object.type !== AST_NODE_TYPES.Identifier ||
84
+ !fastifyVars.has(callee.object.name)
85
+ ) {
86
+ return null;
87
+ }
88
+
89
+ if (callee.property.type !== AST_NODE_TYPES.Identifier) {
90
+ return null;
91
+ }
92
+
93
+ const method = callee.property.name;
94
+
95
+ return ROUTE_METHODS.has(method) ? method : null;
96
+ }
97
+
98
+ export function findRouteOptionsArg(
99
+ node: TSESTree.CallExpression
100
+ ): TSESTree.ObjectExpression | null {
101
+ const firstArg = node.arguments[0];
102
+
103
+ if (
104
+ firstArg?.type === AST_NODE_TYPES.ObjectExpression &&
105
+ node.arguments.length >= 2
106
+ ) {
107
+ return firstArg;
108
+ }
109
+
110
+ const secondArg = node.arguments[1];
111
+
112
+ if (secondArg?.type === AST_NODE_TYPES.ObjectExpression) {
113
+ return secondArg;
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ export function findObjectProperty(
120
+ object: TSESTree.ObjectExpression,
121
+ keyName: string
122
+ ): TSESTree.Property | null {
123
+ for (const prop of object.properties) {
124
+ if (prop.type !== AST_NODE_TYPES.Property || prop.computed) {
125
+ continue;
126
+ }
127
+
128
+ const key = prop.key;
129
+
130
+ if (key.type === AST_NODE_TYPES.Identifier && key.name === keyName) {
131
+ return prop;
132
+ }
133
+
134
+ if (key.type === AST_NODE_TYPES.Literal && key.value === keyName) {
135
+ return prop;
136
+ }
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ export function findNestedProperty(
143
+ object: TSESTree.ObjectExpression,
144
+ ...keys: string[]
145
+ ): TSESTree.Property | null {
146
+ let current: TSESTree.ObjectExpression | null = object;
147
+
148
+ for (let index = 0; index < keys.length; index += 1) {
149
+ const keyName = keys[index];
150
+
151
+ if (current === null || keyName === undefined) {
152
+ return null;
153
+ }
154
+
155
+ const prop = findObjectProperty(current, keyName);
156
+
157
+ if (prop === null) {
158
+ return null;
159
+ }
160
+
161
+ if (index === keys.length - 1) {
162
+ return prop;
163
+ }
164
+
165
+ if (prop.value.type === AST_NODE_TYPES.ObjectExpression) {
166
+ current = prop.value;
167
+ } else {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ return null;
173
+ }
174
+
175
+ export function getRouteHandler(
176
+ node: TSESTree.CallExpression
177
+ ): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | null {
178
+ for (const arg of node.arguments) {
179
+ if (
180
+ arg.type === AST_NODE_TYPES.ArrowFunctionExpression ||
181
+ arg.type === AST_NODE_TYPES.FunctionExpression
182
+ ) {
183
+ return arg;
184
+ }
185
+ }
186
+
187
+ return null;
188
+ }
189
+
190
+ export function nodeContainsCallNamed(
191
+ root: TSESTree.Node,
192
+ objectName: string,
193
+ methodName: string
194
+ ): boolean {
195
+ return walkSome(root, (node) => {
196
+ if (node.type !== AST_NODE_TYPES.CallExpression) {
197
+ return false;
198
+ }
199
+
200
+ const callee = node.callee;
201
+
202
+ return (
203
+ callee.type === AST_NODE_TYPES.MemberExpression &&
204
+ !callee.computed &&
205
+ callee.object.type === AST_NODE_TYPES.Identifier &&
206
+ callee.object.name === objectName &&
207
+ callee.property.type === AST_NODE_TYPES.Identifier &&
208
+ callee.property.name === methodName
209
+ );
210
+ });
211
+ }
212
+
213
+ export function nodeContainsMemberCall(
214
+ root: TSESTree.Node,
215
+ methodName: string
216
+ ): boolean {
217
+ return walkSome(root, (node) => {
218
+ if (node.type !== AST_NODE_TYPES.CallExpression) {
219
+ return false;
220
+ }
221
+
222
+ const callee = node.callee;
223
+
224
+ return (
225
+ callee.type === AST_NODE_TYPES.MemberExpression &&
226
+ !callee.computed &&
227
+ callee.property.type === AST_NODE_TYPES.Identifier &&
228
+ callee.property.name === methodName
229
+ );
230
+ });
231
+ }
@@ -1,30 +1,37 @@
1
1
  import type { TSESLint } from "@typescript-eslint/utils";
2
2
 
3
3
  import type { IRulePack } from "./rule-packs.types";
4
+ import { authorizationPack } from "./authorization";
4
5
  import { bullmqPack } from "./bullmq";
5
6
  import { commentHygienePack } from "./comment-hygiene";
6
7
  import { codeFlowPack } from "./code-flow";
7
8
  import { drizzlePack } from "./drizzle";
8
9
  import { elysiaPack } from "./elysia";
9
10
  import { envAccessPack } from "./env-access";
11
+ import { fastifyPack } from "./fastify";
10
12
  import { i18nKeysPack } from "./i18n-keys";
11
13
  import { jwtCookiesPack } from "./jwt-cookies";
12
14
  import { moduleBoundariesPack } from "./module-boundaries";
13
15
  import { nextjsPack } from "./nextjs";
14
16
  import { oauthSecurityPack } from "./oauth-security";
15
17
  import { reactComponentArchitecturePack } from "./react-component-architecture";
18
+ import { runtimeBoundariesPack } from "./runtime-boundaries";
19
+ import { securityPack } from "./security";
16
20
  import { structuredLoggingPack } from "./structured-logging";
17
21
  import { tanstackQueryPack } from "./tanstack-query";
18
22
  import { testConventionsPack } from "./test-conventions";
23
+ import { typescriptCorePack } from "./typescript-core";
19
24
  import { PACK_REGISTRY } from "../stack-detection";
20
25
 
21
26
  /** Registry of all available rule packs, keyed by pack ID. */
22
27
  export const RULE_PACKS = {
28
+ authorization: authorizationPack,
23
29
  bullmq: bullmqPack,
24
30
  "code-flow": codeFlowPack,
25
31
  "comment-hygiene": commentHygienePack,
26
32
  drizzle: drizzlePack,
27
33
  elysia: elysiaPack,
34
+ fastify: fastifyPack,
28
35
  "env-access": envAccessPack,
29
36
  "i18n-keys": i18nKeysPack,
30
37
  "jwt-cookies": jwtCookiesPack,
@@ -32,9 +39,12 @@ export const RULE_PACKS = {
32
39
  nextjs: nextjsPack,
33
40
  "oauth-security": oauthSecurityPack,
34
41
  "react-component-architecture": reactComponentArchitecturePack,
42
+ "runtime-boundaries": runtimeBoundariesPack,
43
+ security: securityPack,
35
44
  "structured-logging": structuredLoggingPack,
36
45
  "tanstack-query": tanstackQueryPack,
37
46
  "test-conventions": testConventionsPack,
47
+ "typescript-core": typescriptCorePack,
38
48
  } as const;
39
49
 
40
50
  export type IRulePackId = keyof typeof RULE_PACKS;
@@ -2,13 +2,20 @@ import type { TSESLint } from "@typescript-eslint/utils";
2
2
 
3
3
  import { authCookieMustBeHttpOnlyRule } from "./rules/auth-cookie-must-be-httponly";
4
4
  import { authCookieMustBeSecureInProdRule } from "./rules/auth-cookie-must-be-secure-in-prod";
5
+ import { authCookieMustSetMaxAgeOrExpiresRule } from "./rules/auth-cookie-must-set-maxage-or-expires";
6
+ import { authCookieMustSetSameSiteRule } from "./rules/auth-cookie-must-set-samesite";
5
7
  import { bcryptRoundsMinRule } from "./rules/bcrypt-rounds-min";
8
+ import { jwtMustVerifyNotDecodeRule } from "./rules/jwt-must-verify-not-decode";
6
9
  import type { IRulePack } from "../rule-packs.types";
7
10
 
8
11
  const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
9
12
  "auth-cookie-must-be-httponly": authCookieMustBeHttpOnlyRule,
10
13
  "auth-cookie-must-be-secure-in-prod": authCookieMustBeSecureInProdRule,
14
+ "auth-cookie-must-set-maxage-or-expires":
15
+ authCookieMustSetMaxAgeOrExpiresRule,
16
+ "auth-cookie-must-set-samesite": authCookieMustSetSameSiteRule,
11
17
  "bcrypt-rounds-min": bcryptRoundsMinRule,
18
+ "jwt-must-verify-not-decode": jwtMustVerifyNotDecodeRule,
12
19
  };
13
20
 
14
21
  export const jwtCookiesPack: IRulePack = {
@@ -18,7 +25,10 @@ export const jwtCookiesPack: IRulePack = {
18
25
  rulesConfig: {
19
26
  "auth-cookie-must-be-httponly": "error",
20
27
  "auth-cookie-must-be-secure-in-prod": "error",
28
+ "auth-cookie-must-set-maxage-or-expires": "warn",
29
+ "auth-cookie-must-set-samesite": "error",
21
30
  "bcrypt-rounds-min": "error",
31
+ "jwt-must-verify-not-decode": "error",
22
32
  },
23
33
  };
24
34
 
@@ -0,0 +1,132 @@
1
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
2
+
3
+ import { createRule } from "../../create-rule";
4
+ import {
5
+ DEFAULT_AUTH_COOKIE_NAMES,
6
+ DEFAULT_SET_COOKIE_FUNCTIONS,
7
+ DEFAULT_TRUSTED_CONFIG_NAMES,
8
+ lookupCookieOption,
9
+ matchAuthCookieSet,
10
+ } from "../utils";
11
+
12
+ export const RULE_NAME = "auth-cookie-must-set-maxage-or-expires";
13
+
14
+ export interface IAuthCookieMustSetMaxAgeOrExpiresOptions {
15
+ readonly authCookieNames?: readonly string[];
16
+ readonly trustedConfigNames?: readonly string[];
17
+ readonly setCookieFunctions?: readonly string[];
18
+ }
19
+
20
+ type RuleOptions = [IAuthCookieMustSetMaxAgeOrExpiresOptions];
21
+ type MessageIds = "missingLifetime";
22
+
23
+ const optionSchema: JSONSchema4 = {
24
+ type: "object",
25
+ additionalProperties: false,
26
+ properties: {
27
+ authCookieNames: {
28
+ type: "array",
29
+ items: { type: "string" },
30
+ uniqueItems: true,
31
+ minItems: 1,
32
+ },
33
+ trustedConfigNames: {
34
+ type: "array",
35
+ items: { type: "string" },
36
+ uniqueItems: true,
37
+ },
38
+ setCookieFunctions: {
39
+ type: "array",
40
+ items: { type: "string" },
41
+ uniqueItems: true,
42
+ minItems: 1,
43
+ },
44
+ },
45
+ };
46
+
47
+ export const authCookieMustSetMaxAgeOrExpiresRule = createRule<
48
+ RuleOptions,
49
+ MessageIds
50
+ >({
51
+ name: RULE_NAME,
52
+ meta: {
53
+ type: "suggestion",
54
+ docs: {
55
+ description:
56
+ "Auth-cookie writes should set `maxAge` or `expires` so session cookies do not live forever by default.",
57
+ },
58
+ schema: [optionSchema],
59
+ messages: {
60
+ missingLifetime:
61
+ "Auth cookie '{{name}}' missing `maxAge` or `expires` — session cookies without a lifetime persist until cleared.",
62
+ },
63
+ },
64
+ defaultOptions: [
65
+ {
66
+ authCookieNames: [...DEFAULT_AUTH_COOKIE_NAMES],
67
+ trustedConfigNames: [...DEFAULT_TRUSTED_CONFIG_NAMES],
68
+ setCookieFunctions: [...DEFAULT_SET_COOKIE_FUNCTIONS],
69
+ },
70
+ ],
71
+ create(context, [options]) {
72
+ const authCookieNames = new Set(
73
+ options.authCookieNames ?? DEFAULT_AUTH_COOKIE_NAMES
74
+ );
75
+ const trustedConfigNames = new Set(
76
+ options.trustedConfigNames ?? DEFAULT_TRUSTED_CONFIG_NAMES
77
+ );
78
+ const setCookieFunctions = new Set(
79
+ options.setCookieFunctions ?? DEFAULT_SET_COOKIE_FUNCTIONS
80
+ );
81
+
82
+ return {
83
+ CallExpression(node) {
84
+ const match = matchAuthCookieSet(
85
+ node,
86
+ authCookieNames,
87
+ setCookieFunctions
88
+ );
89
+
90
+ if (match === null) {
91
+ return;
92
+ }
93
+
94
+ if (match.optionsNode === null) {
95
+ context.report({
96
+ node,
97
+ messageId: "missingLifetime",
98
+ data: { name: match.cookieName },
99
+ });
100
+
101
+ return;
102
+ }
103
+
104
+ const maxAge = lookupCookieOption(
105
+ match.optionsNode,
106
+ "maxAge",
107
+ trustedConfigNames
108
+ );
109
+ const expires = lookupCookieOption(
110
+ match.optionsNode,
111
+ "expires",
112
+ trustedConfigNames
113
+ );
114
+
115
+ if (
116
+ maxAge.hasTrustedSpread ||
117
+ expires.hasTrustedSpread ||
118
+ maxAge.value !== null ||
119
+ expires.value !== null
120
+ ) {
121
+ return;
122
+ }
123
+
124
+ context.report({
125
+ node,
126
+ messageId: "missingLifetime",
127
+ data: { name: match.cookieName },
128
+ });
129
+ },
130
+ };
131
+ },
132
+ });
@@ -0,0 +1,151 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ DEFAULT_AUTH_COOKIE_NAMES,
7
+ DEFAULT_SET_COOKIE_FUNCTIONS,
8
+ DEFAULT_TRUSTED_CONFIG_NAMES,
9
+ lookupCookieOption,
10
+ matchAuthCookieSet,
11
+ } from "../utils";
12
+
13
+ export const RULE_NAME = "auth-cookie-must-set-samesite";
14
+
15
+ export interface IAuthCookieMustSetSameSiteOptions {
16
+ readonly authCookieNames?: readonly string[];
17
+ readonly trustedConfigNames?: readonly string[];
18
+ readonly setCookieFunctions?: readonly string[];
19
+ }
20
+
21
+ type RuleOptions = [IAuthCookieMustSetSameSiteOptions];
22
+ type MessageIds = "missingSameSite";
23
+
24
+ const optionSchema: JSONSchema4 = {
25
+ type: "object",
26
+ additionalProperties: false,
27
+ properties: {
28
+ authCookieNames: {
29
+ type: "array",
30
+ items: { type: "string" },
31
+ uniqueItems: true,
32
+ minItems: 1,
33
+ },
34
+ trustedConfigNames: {
35
+ type: "array",
36
+ items: { type: "string" },
37
+ uniqueItems: true,
38
+ },
39
+ setCookieFunctions: {
40
+ type: "array",
41
+ items: { type: "string" },
42
+ uniqueItems: true,
43
+ minItems: 1,
44
+ },
45
+ },
46
+ };
47
+
48
+ export const authCookieMustSetSameSiteRule = createRule<
49
+ RuleOptions,
50
+ MessageIds
51
+ >({
52
+ name: RULE_NAME,
53
+ meta: {
54
+ type: "problem",
55
+ docs: {
56
+ description:
57
+ "Auth-cookie writes must set `sameSite` (`strict` or `lax`) — missing SameSite allows cross-site cookie delivery.",
58
+ },
59
+ schema: [optionSchema],
60
+ messages: {
61
+ missingSameSite:
62
+ "Auth cookie '{{name}}' missing `sameSite` — set `sameSite: 'strict'` or `'lax'` to limit cross-site delivery.",
63
+ },
64
+ },
65
+ defaultOptions: [
66
+ {
67
+ authCookieNames: [...DEFAULT_AUTH_COOKIE_NAMES],
68
+ trustedConfigNames: [...DEFAULT_TRUSTED_CONFIG_NAMES],
69
+ setCookieFunctions: [...DEFAULT_SET_COOKIE_FUNCTIONS],
70
+ },
71
+ ],
72
+ create(context, [options]) {
73
+ const authCookieNames = new Set(
74
+ options.authCookieNames ?? DEFAULT_AUTH_COOKIE_NAMES
75
+ );
76
+ const trustedConfigNames = new Set(
77
+ options.trustedConfigNames ?? DEFAULT_TRUSTED_CONFIG_NAMES
78
+ );
79
+ const setCookieFunctions = new Set(
80
+ options.setCookieFunctions ?? DEFAULT_SET_COOKIE_FUNCTIONS
81
+ );
82
+
83
+ return {
84
+ CallExpression(node) {
85
+ const match = matchAuthCookieSet(
86
+ node,
87
+ authCookieNames,
88
+ setCookieFunctions
89
+ );
90
+
91
+ if (match === null) {
92
+ return;
93
+ }
94
+
95
+ if (match.optionsNode === null) {
96
+ context.report({
97
+ node,
98
+ messageId: "missingSameSite",
99
+ data: { name: match.cookieName },
100
+ });
101
+
102
+ return;
103
+ }
104
+
105
+ const { value, hasTrustedSpread } = lookupCookieOption(
106
+ match.optionsNode,
107
+ "sameSite",
108
+ trustedConfigNames
109
+ );
110
+
111
+ if (hasTrustedSpread) {
112
+ if (
113
+ value !== null &&
114
+ value.type === AST_NODE_TYPES.Literal &&
115
+ value.value === "none"
116
+ ) {
117
+ context.report({
118
+ node: value,
119
+ messageId: "missingSameSite",
120
+ data: { name: match.cookieName },
121
+ });
122
+ }
123
+
124
+ return;
125
+ }
126
+
127
+ if (value === null) {
128
+ context.report({
129
+ node,
130
+ messageId: "missingSameSite",
131
+ data: { name: match.cookieName },
132
+ });
133
+
134
+ return;
135
+ }
136
+
137
+ if (
138
+ value.type === AST_NODE_TYPES.Literal &&
139
+ value.value !== "strict" &&
140
+ value.value !== "lax"
141
+ ) {
142
+ context.report({
143
+ node: value,
144
+ messageId: "missingSameSite",
145
+ data: { name: match.cookieName },
146
+ });
147
+ }
148
+ },
149
+ };
150
+ },
151
+ });