@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 { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
2
+ import {
3
+ collectWorkflowPermissionsLines,
4
+ hasWorkflowLevelPermissions,
5
+ } from "../../utils/workflow-yaml";
6
+
7
+ const BROAD_PERMISSION_PATTERN =
8
+ /^ {2}(?:contents|id-token):\s*write\s*(?:#.*)?$/u;
9
+
10
+ export const workflowPermissionsLeastPrivilegeRule: IMetaRule = {
11
+ id: "workflow-permissions-least-privilege",
12
+ category: "ci",
13
+ description:
14
+ "Warn when workflow-level permissions grant contents: write or id-token: write.",
15
+ severity: "warn",
16
+ run({ workflowFiles, readFile }) {
17
+ const violations: IMetaRuleViolation[] = [];
18
+
19
+ for (const file of workflowFiles) {
20
+ const text = readFile(file);
21
+
22
+ if (text === null || !hasWorkflowLevelPermissions(text)) {
23
+ continue;
24
+ }
25
+
26
+ const permissionLines = collectWorkflowPermissionsLines(text);
27
+
28
+ for (const line of permissionLines) {
29
+ if (!BROAD_PERMISSION_PATTERN.test(line)) {
30
+ continue;
31
+ }
32
+
33
+ violations.push({
34
+ file,
35
+ ruleId: "workflow-permissions-least-privilege",
36
+ severity: "warn",
37
+ message: `Workflow-level \`${line.trim()}\` is broader than necessary — scope write permissions to the job that needs them instead of the whole workflow.`,
38
+ });
39
+ }
40
+ }
41
+
42
+ return violations;
43
+ },
44
+ };
@@ -0,0 +1,77 @@
1
+ import { parsePackageJsonObject } from "../../parsers/package-json-parser";
2
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
3
+
4
+ const NEXT_CONFIG_PATHS = [
5
+ "next.config.ts",
6
+ "next.config.js",
7
+ "next.config.mjs",
8
+ "next.config.cjs",
9
+ ] as const;
10
+
11
+ const WILDCARD_HOSTNAME_PATTERN =
12
+ /hostname\s*:\s*['"`]\*\*['"`]|hostname\s*:\s*['"`][^'"`]*\*[^'"`]*['"`]/;
13
+
14
+ function hasNextDependency(
15
+ packageJson: Record<string, unknown> | null
16
+ ): boolean {
17
+ if (packageJson === null) {
18
+ return false;
19
+ }
20
+
21
+ const parsed = parsePackageJsonObject(packageJson);
22
+
23
+ if (parsed === null) {
24
+ return false;
25
+ }
26
+
27
+ const merged: Record<string, string> = {
28
+ ...(parsed.dependencies ?? {}),
29
+ ...(parsed.devDependencies ?? {}),
30
+ };
31
+
32
+ return merged.next !== undefined;
33
+ }
34
+
35
+ function configContainsWildcardRemotePattern(content: string): boolean {
36
+ if (!content.includes("remotePatterns")) {
37
+ return false;
38
+ }
39
+
40
+ return WILDCARD_HOSTNAME_PATTERN.test(content);
41
+ }
42
+
43
+ export const nextImageRemotePatternsNoWildcardsRule: IMetaRule = {
44
+ id: "next-image-remote-patterns-no-wildcards",
45
+ category: "config",
46
+ description:
47
+ "Disallow wildcard hostnames in `images.remotePatterns` — overly broad patterns enable SSRF via next/image.",
48
+ severity: "error",
49
+ appliesTo: ["nextjs"],
50
+ run({ packageJson, readFile }) {
51
+ const violations: IMetaRuleViolation[] = [];
52
+
53
+ if (!hasNextDependency(packageJson)) {
54
+ return violations;
55
+ }
56
+
57
+ for (const path of NEXT_CONFIG_PATHS) {
58
+ const content = readFile(path);
59
+
60
+ if (content === null) {
61
+ continue;
62
+ }
63
+
64
+ if (configContainsWildcardRemotePattern(content)) {
65
+ violations.push({
66
+ file: path,
67
+ ruleId: "next-image-remote-patterns-no-wildcards",
68
+ severity: "error",
69
+ message:
70
+ "`images.remotePatterns` contains a wildcard hostname — list explicit hostnames instead of `**` or `*`-patterns to reduce SSRF risk.",
71
+ });
72
+ }
73
+ }
74
+
75
+ return violations;
76
+ },
77
+ };
@@ -0,0 +1,66 @@
1
+ import { parsePackageJsonObject } from "../../parsers/package-json-parser";
2
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
3
+
4
+ function hasNextDependency(
5
+ packageJson: Record<string, unknown> | null
6
+ ): boolean {
7
+ if (packageJson === null) {
8
+ return false;
9
+ }
10
+
11
+ const parsed = parsePackageJsonObject(packageJson);
12
+
13
+ if (parsed === null) {
14
+ return false;
15
+ }
16
+
17
+ const merged: Record<string, string> = {
18
+ ...(parsed.dependencies ?? {}),
19
+ ...(parsed.devDependencies ?? {}),
20
+ };
21
+
22
+ return merged.next !== undefined;
23
+ }
24
+
25
+ function hasAppRouter(sourceFiles: readonly string[]): boolean {
26
+ return sourceFiles.some(
27
+ (file) => file.startsWith("app/") || file.startsWith("src/app/")
28
+ );
29
+ }
30
+
31
+ export const nextInstrumentationPresentRule: IMetaRule = {
32
+ id: "next-instrumentation-present",
33
+ category: "config",
34
+ description:
35
+ "Recommend instrumentation.ts for OpenTelemetry when using the Next.js app router.",
36
+ severity: "warn",
37
+ appliesTo: ["nextjs"],
38
+ run({ packageJson, sourceFiles, readFile }) {
39
+ const violations: IMetaRuleViolation[] = [];
40
+
41
+ if (!hasNextDependency(packageJson) || !hasAppRouter(sourceFiles)) {
42
+ return violations;
43
+ }
44
+
45
+ const instrumentationPaths = [
46
+ "instrumentation.ts",
47
+ "src/instrumentation.ts",
48
+ ] as const;
49
+
50
+ const hasInstrumentation = instrumentationPaths.some(
51
+ (path) => readFile(path) !== null
52
+ );
53
+
54
+ if (!hasInstrumentation) {
55
+ violations.push({
56
+ file: "instrumentation.ts",
57
+ ruleId: "next-instrumentation-present",
58
+ severity: "warn",
59
+ message:
60
+ "Add instrumentation.ts at the project root (or src/) with registerOTel for OpenTelemetry tracing of Server Components and route handlers.",
61
+ });
62
+ }
63
+
64
+ return violations;
65
+ },
66
+ };
@@ -0,0 +1,64 @@
1
+ import { parsePackageJsonObject } from "../../parsers/package-json-parser";
2
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
3
+
4
+ function hasNextDependency(
5
+ packageJson: Record<string, unknown> | null
6
+ ): boolean {
7
+ if (packageJson === null) {
8
+ return false;
9
+ }
10
+
11
+ const parsed = parsePackageJsonObject(packageJson);
12
+
13
+ if (parsed === null) {
14
+ return false;
15
+ }
16
+
17
+ const merged: Record<string, string> = {
18
+ ...(parsed.dependencies ?? {}),
19
+ ...(parsed.devDependencies ?? {}),
20
+ };
21
+
22
+ return merged.next !== undefined;
23
+ }
24
+
25
+ function fileExists(
26
+ readFile: (relPath: string) => string | null,
27
+ paths: readonly string[]
28
+ ): boolean {
29
+ return paths.some((path) => readFile(path) !== null);
30
+ }
31
+
32
+ export const nextProxyOverMiddlewareRule: IMetaRule = {
33
+ id: "next-proxy-over-middleware",
34
+ category: "config",
35
+ description:
36
+ "When using Next.js 16+, prefer proxy.ts over legacy middleware.ts for early request interception.",
37
+ severity: "warn",
38
+ appliesTo: ["nextjs"],
39
+ run({ packageJson, readFile }) {
40
+ const violations: IMetaRuleViolation[] = [];
41
+
42
+ if (!hasNextDependency(packageJson)) {
43
+ return violations;
44
+ }
45
+
46
+ const middlewarePaths = ["middleware.ts", "src/middleware.ts"] as const;
47
+ const proxyPaths = ["proxy.ts", "src/proxy.ts"] as const;
48
+
49
+ const hasMiddleware = fileExists(readFile, middlewarePaths);
50
+ const hasProxy = fileExists(readFile, proxyPaths);
51
+
52
+ if (hasMiddleware && !hasProxy) {
53
+ violations.push({
54
+ file: "middleware.ts",
55
+ ruleId: "next-proxy-over-middleware",
56
+ severity: "warn",
57
+ message:
58
+ "middleware.ts is legacy — migrate early request interception to proxy.ts (Next.js 16 Node.js-native routing boundary).",
59
+ });
60
+ }
61
+
62
+ return violations;
63
+ },
64
+ };
@@ -0,0 +1,75 @@
1
+ import { join } from "node:path";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
4
+
5
+ /** Narrow `unknown` to a record without a type assertion. */
6
+ function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return typeof value === "object" && value !== null;
8
+ }
9
+
10
+ /** Strip block and line comments from JSON before parsing. */
11
+ function stripJsonComments(text: string): string {
12
+ return text
13
+ .replace(/\/\*[\s\S]*?\*\//gu, "")
14
+ .replace(/^\s*\/\/.*$/gmu, "")
15
+ .replace(/,\s*([\]}])/gu, "$1");
16
+ }
17
+
18
+ const RECOMMENDED_FLAGS: readonly { flag: string; label: string }[] = [
19
+ { flag: "useUnknownInCatchVariables", label: "useUnknownInCatchVariables" },
20
+ { flag: "erasableSyntaxOnly", label: "erasableSyntaxOnly" },
21
+ { flag: "exactOptionalPropertyTypes", label: "exactOptionalPropertyTypes" },
22
+ { flag: "verbatimModuleSyntax", label: "verbatimModuleSyntax" },
23
+ {
24
+ flag: "noPropertyAccessFromIndexSignature",
25
+ label: "noPropertyAccessFromIndexSignature",
26
+ },
27
+ ];
28
+
29
+ export const tsconfigRecommendedFlagsRule: IMetaRule = {
30
+ id: "tsconfig-recommended-flags",
31
+ category: "config",
32
+ description:
33
+ "tsconfig.json should enable recommended strict-adjacent compiler flags (useUnknownInCatchVariables, erasableSyntaxOnly, exactOptionalPropertyTypes, verbatimModuleSyntax, noPropertyAccessFromIndexSignature).",
34
+ severity: "warn",
35
+ run({ root }) {
36
+ const violations: IMetaRuleViolation[] = [];
37
+ const tsconfigPath = join(root, "tsconfig.json");
38
+
39
+ try {
40
+ statSync(tsconfigPath);
41
+ } catch {
42
+ return violations;
43
+ }
44
+
45
+ try {
46
+ const text = readFileSync(tsconfigPath, "utf8");
47
+ const parsed: unknown = JSON.parse(stripJsonComments(text));
48
+
49
+ if (!isRecord(parsed)) {
50
+ return violations;
51
+ }
52
+
53
+ const compilerOptions = parsed.compilerOptions;
54
+
55
+ if (!isRecord(compilerOptions)) {
56
+ return violations;
57
+ }
58
+
59
+ for (const { flag, label } of RECOMMENDED_FLAGS) {
60
+ if (compilerOptions[flag] !== true) {
61
+ violations.push({
62
+ file: "tsconfig.json",
63
+ ruleId: "tsconfig-recommended-flags",
64
+ severity: "warn",
65
+ message: `tsconfig.json compilerOptions should set "${label}": true for stricter, more predictable TypeScript behavior.`,
66
+ });
67
+ }
68
+ }
69
+ } catch {
70
+ // Invalid JSON or file read error — skip this check
71
+ }
72
+
73
+ return violations;
74
+ },
75
+ };
@@ -0,0 +1,61 @@
1
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
2
+
3
+ const OVERRIDE_KEY_PATTERN = /^(\s*)"(?:overrides|resolutions)"\s*:/u;
4
+ const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/u;
5
+
6
+ function hasAdjacentComment(lines: readonly string[], index: number): boolean {
7
+ const line = lines[index];
8
+
9
+ if (line === undefined) {
10
+ return false;
11
+ }
12
+
13
+ if (/\/\/.*$/u.test(line)) {
14
+ return true;
15
+ }
16
+
17
+ const previous = lines[index - 1];
18
+
19
+ return previous !== undefined && LINE_COMMENT_PATTERN.test(previous);
20
+ }
21
+
22
+ export const dependencyOverridesRequireCommentRule: IMetaRule = {
23
+ id: "dependency-overrides-require-comment",
24
+ category: "supply-chain",
25
+ description:
26
+ "overrides/resolutions in package.json must include an adjacent comment explaining why.",
27
+ severity: "warn",
28
+ run({ readFile }) {
29
+ const violations: IMetaRuleViolation[] = [];
30
+ const text = readFile("package.json");
31
+
32
+ if (text === null) {
33
+ return violations;
34
+ }
35
+
36
+ const lines = text.split("\n");
37
+
38
+ for (let index = 0; index < lines.length; index += 1) {
39
+ const line = lines[index];
40
+
41
+ if (line === undefined || !OVERRIDE_KEY_PATTERN.test(line)) {
42
+ continue;
43
+ }
44
+
45
+ if (hasAdjacentComment(lines, index)) {
46
+ continue;
47
+ }
48
+
49
+ const keyName = line.includes("overrides") ? "overrides" : "resolutions";
50
+
51
+ violations.push({
52
+ file: "package.json",
53
+ ruleId: "dependency-overrides-require-comment",
54
+ severity: "warn",
55
+ message: `"${keyName}" is declared without an adjacent comment — document why the override is required (security patch, upstream bug, etc.).`,
56
+ });
57
+ }
58
+
59
+ return violations;
60
+ },
61
+ };
@@ -0,0 +1,54 @@
1
+ import { parsePackageJsonObject } from "../../parsers/package-json-parser";
2
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
3
+
4
+ const SECURITY_PLUGINS = [
5
+ "@fastify/helmet",
6
+ "@fastify/cors",
7
+ "@fastify/rate-limit",
8
+ ] as const;
9
+
10
+ export const fastifySecurityPluginsRule: IMetaRule = {
11
+ id: "fastify-security-plugins",
12
+ category: "supply-chain",
13
+ description:
14
+ "When fastify is a dependency, recommend official security plugins (@fastify/helmet, @fastify/cors, @fastify/rate-limit).",
15
+ severity: "warn",
16
+ appliesTo: ["fastify"],
17
+ run({ packageJson }) {
18
+ const violations: IMetaRuleViolation[] = [];
19
+
20
+ if (packageJson === null) {
21
+ return violations;
22
+ }
23
+
24
+ const parsed = parsePackageJsonObject(packageJson);
25
+
26
+ if (parsed === null) {
27
+ return violations;
28
+ }
29
+
30
+ const merged: Record<string, string> = {
31
+ ...(parsed.dependencies ?? {}),
32
+ ...(parsed.devDependencies ?? {}),
33
+ };
34
+
35
+ if (merged.fastify === undefined) {
36
+ return violations;
37
+ }
38
+
39
+ const missing = SECURITY_PLUGINS.filter((pkg) => merged[pkg] === undefined);
40
+
41
+ if (missing.length === 0) {
42
+ return violations;
43
+ }
44
+
45
+ violations.push({
46
+ file: "package.json",
47
+ ruleId: "fastify-security-plugins",
48
+ severity: "warn",
49
+ message: `fastify is listed but security plugins are missing: ${missing.join(", ")}. Register @fastify/helmet, @fastify/cors, and @fastify/rate-limit at the production boundary.`,
50
+ });
51
+
52
+ return violations;
53
+ },
54
+ };
@@ -0,0 +1,51 @@
1
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
2
+ import {
3
+ detectPresentLockfiles,
4
+ hasLockfileForManager,
5
+ resolvePackageManager,
6
+ } from "../../utils/lockfiles";
7
+
8
+ const MANAGER_LABELS = {
9
+ npm: "package-lock.json",
10
+ yarn: "yarn.lock",
11
+ pnpm: "pnpm-lock.yaml",
12
+ bun: "bun.lockb or bun.lock",
13
+ } as const;
14
+
15
+ export const lockfileRequiredRule: IMetaRule = {
16
+ id: "lockfile-required",
17
+ category: "supply-chain",
18
+ description:
19
+ "Projects must commit exactly one lockfile matching the detected package manager.",
20
+ severity: "warn",
21
+ run({ root, packageJson }) {
22
+ const violations: IMetaRuleViolation[] = [];
23
+ const manager = resolvePackageManager(root, packageJson);
24
+ const present = detectPresentLockfiles(root);
25
+
26
+ if (manager === null) {
27
+ if (present.length === 0) {
28
+ violations.push({
29
+ file: "package.json",
30
+ ruleId: "lockfile-required",
31
+ severity: "warn",
32
+ message:
33
+ "No lockfile found — add the lockfile for your package manager (package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb) and set packageManager in package.json.",
34
+ });
35
+ }
36
+
37
+ return violations;
38
+ }
39
+
40
+ if (!hasLockfileForManager(root, manager)) {
41
+ violations.push({
42
+ file: "package.json",
43
+ ruleId: "lockfile-required",
44
+ severity: "warn",
45
+ message: `Detected package manager "${manager}" but missing ${MANAGER_LABELS[manager]} — commit the lockfile produced by your package manager.`,
46
+ });
47
+ }
48
+
49
+ return violations;
50
+ },
51
+ };
@@ -0,0 +1,49 @@
1
+ import { join } from "node:path";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
4
+
5
+ const MIGRATION_DIRS = ["drizzle", "migrations"] as const;
6
+
7
+ function directoryHasFiles(root: string, relDir: string): boolean {
8
+ try {
9
+ const stat = statSync(join(root, relDir));
10
+
11
+ if (!stat.isDirectory()) {
12
+ return false;
13
+ }
14
+
15
+ return readdirSync(join(root, relDir)).length > 0;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ export const migrationsMustBeCheckedInRule: IMetaRule = {
22
+ id: "migrations-must-be-checked-in",
23
+ category: "supply-chain",
24
+ description:
25
+ "When using Drizzle, commit SQL migrations under drizzle/ or migrations/.",
26
+ severity: "warn",
27
+ appliesTo: ["drizzle"],
28
+ run({ root }) {
29
+ const violations: IMetaRuleViolation[] = [];
30
+
31
+ const hasMigrationDir = MIGRATION_DIRS.some((dir) =>
32
+ directoryHasFiles(root, dir)
33
+ );
34
+
35
+ if (hasMigrationDir) {
36
+ return violations;
37
+ }
38
+
39
+ violations.push({
40
+ file: "drizzle/",
41
+ ruleId: "migrations-must-be-checked-in",
42
+ severity: "warn",
43
+ message:
44
+ "No checked-in Drizzle migrations found — add a drizzle/ or migrations/ folder with generated SQL migration files.",
45
+ });
46
+
47
+ return violations;
48
+ },
49
+ };
@@ -0,0 +1,70 @@
1
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
2
+
3
+ /** Narrow `unknown` to a record without a type assertion. */
4
+ function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null;
6
+ }
7
+
8
+ const REMOTE_DEP_PATTERN =
9
+ /^(?:git\+|git:|http:|https:\/\/(?!registry\.npmjs\.org))/iu;
10
+
11
+ const DEP_SECTIONS = [
12
+ "dependencies",
13
+ "devDependencies",
14
+ "optionalDependencies",
15
+ "peerDependencies",
16
+ ] as const;
17
+
18
+ function toStringRecord(value: unknown): Record<string, string> | undefined {
19
+ if (!isRecord(value)) {
20
+ return undefined;
21
+ }
22
+
23
+ const out: Record<string, string> = {};
24
+
25
+ for (const [key, entry] of Object.entries(value)) {
26
+ if (typeof entry === "string") {
27
+ out[key] = entry;
28
+ }
29
+ }
30
+
31
+ return out;
32
+ }
33
+
34
+ export const noGitOrTarballDependenciesRule: IMetaRule = {
35
+ id: "no-git-or-tarball-dependencies",
36
+ category: "supply-chain",
37
+ description:
38
+ "Warn on git+, git:, or http(s) tarball dependency URLs in package.json.",
39
+ severity: "warn",
40
+ run({ packageJson }) {
41
+ const violations: IMetaRuleViolation[] = [];
42
+
43
+ if (packageJson === null) {
44
+ return violations;
45
+ }
46
+
47
+ for (const section of DEP_SECTIONS) {
48
+ const entries = toStringRecord(packageJson[section]);
49
+
50
+ if (entries === undefined) {
51
+ continue;
52
+ }
53
+
54
+ for (const [name, spec] of Object.entries(entries)) {
55
+ if (!REMOTE_DEP_PATTERN.test(spec)) {
56
+ continue;
57
+ }
58
+
59
+ violations.push({
60
+ file: "package.json",
61
+ ruleId: "no-git-or-tarball-dependencies",
62
+ severity: "warn",
63
+ message: `${section}.${name} uses remote URL "${spec}" — prefer registry versions for reproducible installs and supply-chain auditing.`,
64
+ });
65
+ }
66
+ }
67
+
68
+ return violations;
69
+ },
70
+ };
@@ -0,0 +1,31 @@
1
+ import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
2
+
3
+ export const packageManagerFieldRequiredRule: IMetaRule = {
4
+ id: "package-manager-field-required",
5
+ category: "supply-chain",
6
+ description: "package.json must declare a packageManager field.",
7
+ severity: "warn",
8
+ run({ packageJson }) {
9
+ const violations: IMetaRuleViolation[] = [];
10
+
11
+ if (packageJson === null) {
12
+ return violations;
13
+ }
14
+
15
+ const value = packageJson.packageManager;
16
+
17
+ if (typeof value === "string" && value.trim().length > 0) {
18
+ return violations;
19
+ }
20
+
21
+ violations.push({
22
+ file: "package.json",
23
+ ruleId: "package-manager-field-required",
24
+ severity: "warn",
25
+ message:
26
+ 'Add a "packageManager" field to package.json (e.g. "bun@1.3.14") so CI and contributors use the same package manager.',
27
+ });
28
+
29
+ return violations;
30
+ },
31
+ };