@bantay/cli 0.1.0 → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bantay/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Write down the rules your system must never break. We enforce them on every PR.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -133,6 +133,12 @@ async function handleInit(args: string[]) {
133
133
  console.log(" Auth: Not detected");
134
134
  }
135
135
 
136
+ if (result.detection.payments) {
137
+ console.log(` Payments: ${result.detection.payments.name} (${result.detection.payments.confidence} confidence)`);
138
+ } else {
139
+ console.log(" Payments: Not detected");
140
+ }
141
+
136
142
  console.log("");
137
143
 
138
144
  // Display warnings
@@ -0,0 +1,92 @@
1
+ import { readFile, access } from "fs/promises";
2
+ import { join } from "path";
3
+ import type { AuthDetection } from "./types";
4
+
5
+ async function fileExists(path: string): Promise<boolean> {
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ async function readPackageJson(projectPath: string): Promise<Record<string, unknown> | null> {
15
+ try {
16
+ const content = await readFile(join(projectPath, "package.json"), "utf-8");
17
+ return JSON.parse(content);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export async function detect(projectPath: string): Promise<AuthDetection | null> {
24
+ const pkg = await readPackageJson(projectPath);
25
+
26
+ let version: string | undefined;
27
+ let confidence: "high" | "medium" | "low" = "low";
28
+ let detected = false;
29
+ let authFunction: string | undefined;
30
+ let sessionFunction: string | undefined;
31
+
32
+ if (pkg) {
33
+ const deps = (pkg.dependencies ?? {}) as Record<string, string>;
34
+ const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
35
+
36
+ // Check for next-auth (Auth.js v4)
37
+ if (deps["next-auth"]) {
38
+ version = deps["next-auth"];
39
+ confidence = "high";
40
+ detected = true;
41
+ authFunction = "getServerSession";
42
+ sessionFunction = "getServerSession(authOptions)";
43
+ }
44
+
45
+ // Check for @auth/nextjs (Auth.js v5)
46
+ if (deps["@auth/nextjs-auth"]) {
47
+ version = deps["@auth/nextjs-auth"];
48
+ confidence = "high";
49
+ detected = true;
50
+ authFunction = "auth";
51
+ sessionFunction = "auth()";
52
+ }
53
+
54
+ // Check for auth.js v5 with new package name
55
+ if (deps["next-auth"] && version?.startsWith("5")) {
56
+ authFunction = "auth";
57
+ sessionFunction = "auth()";
58
+ }
59
+ }
60
+
61
+ // Check for auth config files
62
+ const configFiles = [
63
+ "auth.ts",
64
+ "auth.config.ts",
65
+ "lib/auth.ts",
66
+ "src/auth.ts",
67
+ "src/lib/auth.ts",
68
+ "app/api/auth/[...nextauth]/route.ts",
69
+ ];
70
+
71
+ for (const configFile of configFiles) {
72
+ if (await fileExists(join(projectPath, configFile))) {
73
+ detected = true;
74
+ if (confidence === "low") {
75
+ confidence = "medium";
76
+ }
77
+ break;
78
+ }
79
+ }
80
+
81
+ if (!detected) {
82
+ return null;
83
+ }
84
+
85
+ return {
86
+ name: "authjs",
87
+ version,
88
+ confidence,
89
+ authFunction,
90
+ sessionFunction,
91
+ };
92
+ }
@@ -0,0 +1,88 @@
1
+ import { readFile, access } from "fs/promises";
2
+ import { join } from "path";
3
+ import type { AuthDetection } from "./types";
4
+
5
+ async function fileExists(path: string): Promise<boolean> {
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ async function readPackageJson(projectPath: string): Promise<Record<string, unknown> | null> {
15
+ try {
16
+ const content = await readFile(join(projectPath, "package.json"), "utf-8");
17
+ return JSON.parse(content);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export async function detect(projectPath: string): Promise<AuthDetection | null> {
24
+ const pkg = await readPackageJson(projectPath);
25
+
26
+ let version: string | undefined;
27
+ let confidence: "high" | "medium" | "low" = "low";
28
+ let detected = false;
29
+
30
+ if (pkg) {
31
+ const deps = (pkg.dependencies ?? {}) as Record<string, string>;
32
+ const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
33
+
34
+ // Check for @clerk/nextjs
35
+ if (deps["@clerk/nextjs"]) {
36
+ version = deps["@clerk/nextjs"];
37
+ confidence = "high";
38
+ detected = true;
39
+ } else if (devDeps["@clerk/nextjs"]) {
40
+ version = devDeps["@clerk/nextjs"];
41
+ confidence = "medium";
42
+ detected = true;
43
+ }
44
+
45
+ // Check for @clerk/clerk-sdk-node
46
+ if (deps["@clerk/clerk-sdk-node"] || devDeps["@clerk/clerk-sdk-node"]) {
47
+ detected = true;
48
+ if (confidence === "low") {
49
+ confidence = "high";
50
+ }
51
+ }
52
+ }
53
+
54
+ // Check for Clerk middleware
55
+ const middlewareFiles = [
56
+ "middleware.ts",
57
+ "middleware.js",
58
+ "src/middleware.ts",
59
+ "src/middleware.js",
60
+ ];
61
+
62
+ for (const middlewareFile of middlewareFiles) {
63
+ if (await fileExists(join(projectPath, middlewareFile))) {
64
+ try {
65
+ const content = await readFile(join(projectPath, middlewareFile), "utf-8");
66
+ if (content.includes("@clerk") || content.includes("clerkMiddleware") || content.includes("authMiddleware")) {
67
+ detected = true;
68
+ confidence = "high";
69
+ break;
70
+ }
71
+ } catch {
72
+ // Ignore read errors
73
+ }
74
+ }
75
+ }
76
+
77
+ if (!detected) {
78
+ return null;
79
+ }
80
+
81
+ return {
82
+ name: "clerk",
83
+ version,
84
+ confidence,
85
+ authFunction: "auth",
86
+ sessionFunction: "auth()",
87
+ };
88
+ }
@@ -0,0 +1,105 @@
1
+ import { readFile, access } from "fs/promises";
2
+ import { join } from "path";
3
+ import type { OrmDetection } from "./types";
4
+
5
+ async function fileExists(path: string): Promise<boolean> {
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ async function readPackageJson(projectPath: string): Promise<Record<string, unknown> | null> {
15
+ try {
16
+ const content = await readFile(join(projectPath, "package.json"), "utf-8");
17
+ return JSON.parse(content);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ async function findSchemaPath(projectPath: string): Promise<string | undefined> {
24
+ // Check common Drizzle schema locations
25
+ const paths = [
26
+ "drizzle/schema.ts",
27
+ "src/db/schema.ts",
28
+ "src/drizzle/schema.ts",
29
+ "lib/db/schema.ts",
30
+ "lib/drizzle/schema.ts",
31
+ "db/schema.ts",
32
+ "schema.ts",
33
+ "src/schema.ts",
34
+ ];
35
+
36
+ for (const path of paths) {
37
+ if (await fileExists(join(projectPath, path))) {
38
+ return path;
39
+ }
40
+ }
41
+
42
+ return undefined;
43
+ }
44
+
45
+ export async function detect(projectPath: string): Promise<OrmDetection | null> {
46
+ const pkg = await readPackageJson(projectPath);
47
+
48
+ let version: string | undefined;
49
+ let confidence: "high" | "medium" | "low" = "low";
50
+ let detected = false;
51
+
52
+ if (pkg) {
53
+ const deps = (pkg.dependencies ?? {}) as Record<string, string>;
54
+ const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
55
+
56
+ // Check for drizzle-orm
57
+ if (deps["drizzle-orm"]) {
58
+ version = deps["drizzle-orm"];
59
+ confidence = "high";
60
+ detected = true;
61
+ } else if (devDeps["drizzle-orm"]) {
62
+ version = devDeps["drizzle-orm"];
63
+ confidence = "medium";
64
+ detected = true;
65
+ }
66
+
67
+ // Check for drizzle-kit (CLI tool)
68
+ if (deps["drizzle-kit"] || devDeps["drizzle-kit"]) {
69
+ detected = true;
70
+ if (confidence === "low") {
71
+ confidence = "high";
72
+ }
73
+ }
74
+ }
75
+
76
+ // Check for drizzle.config.ts
77
+ const configFiles = [
78
+ "drizzle.config.ts",
79
+ "drizzle.config.js",
80
+ "drizzle.config.json",
81
+ ];
82
+
83
+ for (const configFile of configFiles) {
84
+ if (await fileExists(join(projectPath, configFile))) {
85
+ detected = true;
86
+ if (confidence === "low") {
87
+ confidence = "high";
88
+ }
89
+ break;
90
+ }
91
+ }
92
+
93
+ if (!detected) {
94
+ return null;
95
+ }
96
+
97
+ const schemaPath = await findSchemaPath(projectPath);
98
+
99
+ return {
100
+ name: "drizzle",
101
+ version,
102
+ confidence,
103
+ schemaPath,
104
+ };
105
+ }
@@ -2,34 +2,48 @@ export type {
2
2
  FrameworkDetection,
3
3
  OrmDetection,
4
4
  AuthDetection,
5
+ PaymentsDetection,
5
6
  StackDetectionResult,
6
7
  } from "./types";
7
8
 
8
- import type { StackDetectionResult, FrameworkDetection, OrmDetection, AuthDetection } from "./types";
9
+ import type { StackDetectionResult, FrameworkDetection, OrmDetection, AuthDetection, PaymentsDetection } from "./types";
9
10
  import { detect as detectNextjs } from "./nextjs";
10
11
  import { detect as detectPrisma } from "./prisma";
12
+ import { detect as detectDrizzle } from "./drizzle";
13
+ import { detect as detectAuthjs } from "./authjs";
14
+ import { detect as detectClerk } from "./clerk";
15
+ import { detect as detectStripe } from "./stripe";
11
16
 
12
17
  // Registry of framework detectors
13
- const frameworkDetectors: Array<() => typeof detectNextjs> = [
14
- () => detectNextjs,
18
+ const frameworkDetectors: Array<(projectPath: string) => Promise<FrameworkDetection | null>> = [
19
+ detectNextjs,
15
20
  ];
16
21
 
17
22
  // Registry of ORM detectors
18
- const ormDetectors: Array<() => typeof detectPrisma> = [
19
- () => detectPrisma,
23
+ const ormDetectors: Array<(projectPath: string) => Promise<OrmDetection | null>> = [
24
+ detectPrisma,
25
+ detectDrizzle,
20
26
  ];
21
27
 
22
- // Registry of auth detectors (none yet)
23
- const authDetectors: Array<() => (projectPath: string) => Promise<AuthDetection | null>> = [];
28
+ // Registry of auth detectors
29
+ const authDetectors: Array<(projectPath: string) => Promise<AuthDetection | null>> = [
30
+ detectClerk, // Check Clerk first (more specific)
31
+ detectAuthjs,
32
+ ];
33
+
34
+ // Registry of payments detectors
35
+ const paymentsDetectors: Array<(projectPath: string) => Promise<PaymentsDetection | null>> = [
36
+ detectStripe,
37
+ ];
24
38
 
25
39
  export async function detectStack(projectPath: string): Promise<StackDetectionResult> {
26
40
  let framework: FrameworkDetection | null = null;
27
41
  let orm: OrmDetection | null = null;
28
42
  let auth: AuthDetection | null = null;
43
+ let payments: PaymentsDetection | null = null;
29
44
 
30
45
  // Run framework detectors
31
- for (const getDetector of frameworkDetectors) {
32
- const detector = getDetector();
46
+ for (const detector of frameworkDetectors) {
33
47
  const result = await detector(projectPath);
34
48
  if (result && (!framework || result.confidence === "high")) {
35
49
  framework = result;
@@ -38,8 +52,7 @@ export async function detectStack(projectPath: string): Promise<StackDetectionRe
38
52
  }
39
53
 
40
54
  // Run ORM detectors
41
- for (const getDetector of ormDetectors) {
42
- const detector = getDetector();
55
+ for (const detector of ormDetectors) {
43
56
  const result = await detector(projectPath);
44
57
  if (result && (!orm || result.confidence === "high")) {
45
58
  orm = result;
@@ -48,8 +61,7 @@ export async function detectStack(projectPath: string): Promise<StackDetectionRe
48
61
  }
49
62
 
50
63
  // Run auth detectors
51
- for (const getDetector of authDetectors) {
52
- const detector = getDetector();
64
+ for (const detector of authDetectors) {
53
65
  const result = await detector(projectPath);
54
66
  if (result && (!auth || result.confidence === "high")) {
55
67
  auth = result;
@@ -57,5 +69,14 @@ export async function detectStack(projectPath: string): Promise<StackDetectionRe
57
69
  }
58
70
  }
59
71
 
60
- return { framework, orm, auth };
72
+ // Run payments detectors
73
+ for (const detector of paymentsDetectors) {
74
+ const result = await detector(projectPath);
75
+ if (result && (!payments || result.confidence === "high")) {
76
+ payments = result;
77
+ if (result.confidence === "high") break;
78
+ }
79
+ }
80
+
81
+ return { framework, orm, auth, payments };
61
82
  }
@@ -74,18 +74,27 @@ export async function detect(projectPath: string): Promise<FrameworkDetection |
74
74
  return null;
75
75
  }
76
76
 
77
- // Detect router type
77
+ // Detect router type and determine route pattern
78
78
  let router: "app" | "pages" | undefined;
79
+ let routePattern: string | undefined;
79
80
 
80
81
  const hasAppDir = await dirExists(join(projectPath, "app"));
81
82
  const hasPagesDir = await dirExists(join(projectPath, "pages"));
82
83
  const hasSrcAppDir = await dirExists(join(projectPath, "src", "app"));
83
84
  const hasSrcPagesDir = await dirExists(join(projectPath, "src", "pages"));
84
85
 
85
- if (hasAppDir || hasSrcAppDir) {
86
+ if (hasAppDir) {
86
87
  router = "app";
87
- } else if (hasPagesDir || hasSrcPagesDir) {
88
+ routePattern = "app/api/**/route.ts";
89
+ } else if (hasSrcAppDir) {
90
+ router = "app";
91
+ routePattern = "src/app/api/**/route.ts";
92
+ } else if (hasPagesDir) {
93
+ router = "pages";
94
+ routePattern = "pages/api/**/*.ts";
95
+ } else if (hasSrcPagesDir) {
88
96
  router = "pages";
97
+ routePattern = "src/pages/api/**/*.ts";
89
98
  }
90
99
 
91
100
  return {
@@ -93,5 +102,6 @@ export async function detect(projectPath: string): Promise<FrameworkDetection |
93
102
  version,
94
103
  confidence,
95
104
  router,
105
+ routePattern,
96
106
  };
97
107
  }
@@ -0,0 +1,88 @@
1
+ import { readFile, access, readdir } from "fs/promises";
2
+ import { join } from "path";
3
+ import type { PaymentsDetection } from "./types";
4
+
5
+ async function fileExists(path: string): Promise<boolean> {
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ async function readPackageJson(projectPath: string): Promise<Record<string, unknown> | null> {
15
+ try {
16
+ const content = await readFile(join(projectPath, "package.json"), "utf-8");
17
+ return JSON.parse(content);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ async function findWebhookPattern(projectPath: string): Promise<string | undefined> {
24
+ // Check common webhook locations
25
+ const patterns = [
26
+ { path: "app/api/webhooks/stripe/route.ts", pattern: "app/api/webhooks/stripe/route.ts" },
27
+ { path: "app/api/webhook/stripe/route.ts", pattern: "app/api/webhook/stripe/route.ts" },
28
+ { path: "app/api/stripe/webhook/route.ts", pattern: "app/api/stripe/webhook/route.ts" },
29
+ { path: "app/webhooks/stripe/route.ts", pattern: "app/webhooks/stripe/route.ts" },
30
+ { path: "src/app/api/webhooks/stripe/route.ts", pattern: "src/app/api/webhooks/stripe/route.ts" },
31
+ { path: "pages/api/webhooks/stripe.ts", pattern: "pages/api/webhooks/stripe.ts" },
32
+ { path: "pages/api/webhook/stripe.ts", pattern: "pages/api/webhook/stripe.ts" },
33
+ ];
34
+
35
+ for (const { path, pattern } of patterns) {
36
+ if (await fileExists(join(projectPath, path))) {
37
+ return pattern;
38
+ }
39
+ }
40
+
41
+ return undefined;
42
+ }
43
+
44
+ export async function detect(projectPath: string): Promise<PaymentsDetection | null> {
45
+ const pkg = await readPackageJson(projectPath);
46
+
47
+ let version: string | undefined;
48
+ let confidence: "high" | "medium" | "low" = "low";
49
+ let detected = false;
50
+
51
+ // Check package.json for stripe dependency
52
+ if (pkg) {
53
+ const deps = (pkg.dependencies ?? {}) as Record<string, string>;
54
+ const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
55
+
56
+ if (deps.stripe) {
57
+ version = deps.stripe;
58
+ confidence = "high";
59
+ detected = true;
60
+ } else if (devDeps.stripe) {
61
+ version = devDeps.stripe;
62
+ confidence = "medium";
63
+ detected = true;
64
+ }
65
+
66
+ // Also check for @stripe/stripe-js (client-side)
67
+ if (deps["@stripe/stripe-js"] || devDeps["@stripe/stripe-js"]) {
68
+ detected = true;
69
+ if (confidence === "low") {
70
+ confidence = "medium";
71
+ }
72
+ }
73
+ }
74
+
75
+ if (!detected) {
76
+ return null;
77
+ }
78
+
79
+ const webhookPattern = await findWebhookPattern(projectPath);
80
+
81
+ return {
82
+ name: "stripe",
83
+ version,
84
+ confidence,
85
+ webhookPattern,
86
+ secretEnvVar: "STRIPE_SECRET_KEY",
87
+ };
88
+ }
@@ -3,6 +3,7 @@ export interface FrameworkDetection {
3
3
  version?: string;
4
4
  confidence: "high" | "medium" | "low";
5
5
  router?: "app" | "pages";
6
+ routePattern?: string; // e.g., "app/api/**/route.ts" or "pages/api/**/*.ts"
6
7
  }
7
8
 
8
9
  export interface OrmDetection {
@@ -16,12 +17,23 @@ export interface AuthDetection {
16
17
  name: string;
17
18
  version?: string;
18
19
  confidence: "high" | "medium" | "low";
20
+ authFunction?: string; // e.g., "auth()" for Auth.js, "getAuth()" for Clerk
21
+ sessionFunction?: string; // e.g., "getServerSession()" or "currentUser()"
22
+ }
23
+
24
+ export interface PaymentsDetection {
25
+ name: string;
26
+ version?: string;
27
+ confidence: "high" | "medium" | "low";
28
+ webhookPattern?: string; // e.g., "app/api/webhooks/stripe/route.ts"
29
+ secretEnvVar?: string; // e.g., "STRIPE_SECRET_KEY"
19
30
  }
20
31
 
21
32
  export interface StackDetectionResult {
22
33
  framework: FrameworkDetection | null;
23
34
  orm: OrmDetection | null;
24
35
  auth: AuthDetection | null;
36
+ payments: PaymentsDetection | null;
25
37
  }
26
38
 
27
39
  export interface Detector<T> {
@@ -12,62 +12,159 @@ interface InvariantTemplate {
12
12
  statement: string;
13
13
  }
14
14
 
15
- // Universal invariants that apply to all projects
16
- const universalInvariants: InvariantTemplate[] = [
17
- {
18
- id: "INV-001",
19
- category: "security",
20
- statement: "No secrets or credentials committed to version control",
21
- },
22
- {
23
- id: "INV-002",
24
- category: "security",
25
- statement: "All user input must be validated before processing",
26
- },
27
- ];
28
-
29
- // Next.js specific invariants
30
- const nextjsInvariants: InvariantTemplate[] = [
31
- {
32
- id: "INV-010",
33
- category: "auth",
34
- statement: "All API routes must check authentication before processing requests (auth-on-routes)",
35
- },
36
- {
37
- id: "INV-011",
38
- category: "auth",
39
- statement: "Protected pages must redirect unauthenticated users",
40
- },
41
- ];
42
-
43
- // Prisma specific invariants
44
- const prismaInvariants: InvariantTemplate[] = [
45
- {
46
- id: "INV-020",
47
- category: "schema",
48
- statement: "All database tables must have createdAt and updatedAt timestamps (timestamps-on-tables)",
49
- },
50
- {
51
- id: "INV-021",
52
- category: "schema",
53
- statement: "All database tables must use soft-delete pattern with deletedAt column (soft-delete)",
54
- },
55
- {
56
- id: "INV-022",
57
- category: "schema",
58
- statement: "No raw SQL queries - use Prisma client methods only (no-raw-sql)",
59
- },
60
- ];
61
-
15
+ /**
16
+ * Generate stack-specific invariants based on detected components.
17
+ * These are project-specific, checkable rules — not generic security posters.
18
+ */
62
19
  function collectInvariants(stack: StackDetectionResult): InvariantTemplate[] {
63
- const invariants: InvariantTemplate[] = [...universalInvariants];
20
+ const invariants: InvariantTemplate[] = [];
64
21
 
65
- if (stack.framework?.name === "nextjs") {
66
- invariants.push(...nextjsInvariants);
22
+ // Next.js App Router invariants
23
+ if (stack.framework?.name === "nextjs" && stack.framework.router === "app") {
24
+ const routePattern = stack.framework.routePattern || "app/api/**/route.ts";
25
+
26
+ // Auth invariants depend on detected auth library
27
+ if (stack.auth?.name === "clerk") {
28
+ invariants.push({
29
+ id: "inv_route_auth",
30
+ category: "auth",
31
+ statement: `Every ${routePattern} calls auth() from @clerk/nextjs before processing the request`,
32
+ });
33
+ invariants.push({
34
+ id: "inv_server_action_auth",
35
+ category: "auth",
36
+ statement: `Every server action ("use server") calls auth() and checks userId before mutating data`,
37
+ });
38
+ } else if (stack.auth?.name === "authjs") {
39
+ const authFn = stack.auth.sessionFunction || "auth()";
40
+ invariants.push({
41
+ id: "inv_route_auth",
42
+ category: "auth",
43
+ statement: `Every ${routePattern} calls ${authFn} and checks session before processing the request`,
44
+ });
45
+ invariants.push({
46
+ id: "inv_server_action_auth",
47
+ category: "auth",
48
+ statement: `Every server action ("use server") calls ${authFn} and checks session.user before mutating data`,
49
+ });
50
+ } else {
51
+ // No auth detected, use generic but still specific pattern
52
+ invariants.push({
53
+ id: "inv_route_auth",
54
+ category: "auth",
55
+ statement: `Every ${routePattern} verifies authentication before processing the request`,
56
+ });
57
+ }
58
+
59
+ // Middleware invariant if auth detected
60
+ if (stack.auth) {
61
+ invariants.push({
62
+ id: "inv_middleware_matcher",
63
+ category: "auth",
64
+ statement: `middleware.ts config.matcher includes all protected routes — unmatched routes bypass auth`,
65
+ });
66
+ }
67
67
  }
68
68
 
69
+ // Next.js Pages Router invariants
70
+ if (stack.framework?.name === "nextjs" && stack.framework.router === "pages") {
71
+ const routePattern = stack.framework.routePattern || "pages/api/**/*.ts";
72
+
73
+ if (stack.auth?.name === "authjs") {
74
+ invariants.push({
75
+ id: "inv_route_auth",
76
+ category: "auth",
77
+ statement: `Every ${routePattern} calls getServerSession(req, res, authOptions) before processing`,
78
+ });
79
+ } else {
80
+ invariants.push({
81
+ id: "inv_route_auth",
82
+ category: "auth",
83
+ statement: `Every ${routePattern} verifies authentication before processing the request`,
84
+ });
85
+ }
86
+ }
87
+
88
+ // Prisma invariants
69
89
  if (stack.orm?.name === "prisma") {
70
- invariants.push(...prismaInvariants);
90
+ const schemaPath = stack.orm.schemaPath || "prisma/schema.prisma";
91
+ invariants.push({
92
+ id: "inv_model_timestamps",
93
+ category: "schema",
94
+ statement: `Every model in ${schemaPath} has createdAt DateTime @default(now()) and updatedAt DateTime @updatedAt`,
95
+ });
96
+ invariants.push({
97
+ id: "inv_model_soft_delete",
98
+ category: "schema",
99
+ statement: `Every model in ${schemaPath} has deletedAt DateTime? for soft-delete support`,
100
+ });
101
+ invariants.push({
102
+ id: "inv_no_raw_sql",
103
+ category: "schema",
104
+ statement: `No $queryRaw or $executeRaw calls in source files — use Prisma client methods only`,
105
+ });
106
+ }
107
+
108
+ // Drizzle invariants
109
+ if (stack.orm?.name === "drizzle") {
110
+ const schemaPath = stack.orm.schemaPath || "src/db/schema.ts";
111
+ invariants.push({
112
+ id: "inv_table_timestamps",
113
+ category: "schema",
114
+ statement: `Every table in ${schemaPath} has createdAt: timestamp().defaultNow() and updatedAt: timestamp().defaultNow().$onUpdate(() => new Date())`,
115
+ });
116
+ invariants.push({
117
+ id: "inv_table_soft_delete",
118
+ category: "schema",
119
+ statement: `Every table in ${schemaPath} has deletedAt: timestamp() for soft-delete support`,
120
+ });
121
+ }
122
+
123
+ // Stripe invariants
124
+ if (stack.payments?.name === "stripe") {
125
+ const webhookPattern = stack.payments.webhookPattern || "app/api/webhooks/stripe/route.ts";
126
+ invariants.push({
127
+ id: "inv_stripe_webhook_verify",
128
+ category: "payments",
129
+ statement: `${webhookPattern} calls stripe.webhooks.constructEvent() with STRIPE_WEBHOOK_SECRET before processing any event`,
130
+ });
131
+ invariants.push({
132
+ id: "inv_stripe_secret_server",
133
+ category: "payments",
134
+ statement: `STRIPE_SECRET_KEY is only accessed in server-side code — never imported in files under app/**/page.tsx or components/`,
135
+ });
136
+ invariants.push({
137
+ id: "inv_stripe_idempotency",
138
+ category: "payments",
139
+ statement: `Every stripe.charges.create() and stripe.subscriptions.create() call includes idempotencyKey parameter`,
140
+ });
141
+ }
142
+
143
+ // Logging invariants (if any ORM detected, likely has user data)
144
+ if (stack.orm) {
145
+ invariants.push({
146
+ id: "inv_no_pii_logs",
147
+ category: "logging",
148
+ statement: `No console.log, logger.info, or logger.error call includes email, password, ssn, creditCard, or token fields`,
149
+ });
150
+ }
151
+
152
+ // Environment invariants
153
+ if (stack.framework?.name === "nextjs") {
154
+ invariants.push({
155
+ id: "inv_env_no_commit",
156
+ category: "security",
157
+ statement: `.env and .env.local are in .gitignore — only .env.example with placeholder values is committed`,
158
+ });
159
+ }
160
+
161
+ // If no stack detected, provide minimal but still specific invariants
162
+ if (invariants.length === 0) {
163
+ invariants.push({
164
+ id: "inv_env_no_commit",
165
+ category: "security",
166
+ statement: `.env files are in .gitignore — secrets never committed to version control`,
167
+ });
71
168
  }
72
169
 
73
170
  return invariants;
@@ -96,17 +193,36 @@ export async function generateInvariants(stack: StackDetectionResult): Promise<s
96
193
  const lines: string[] = [
97
194
  "# Project Invariants",
98
195
  "",
99
- "This file defines the invariants that must hold for this project.",
100
- "Each invariant is checked by `bantay check` on every PR.",
196
+ "Rules that must hold for this codebase. Checked by `bantay check` on every PR.",
101
197
  "",
102
198
  ];
103
199
 
200
+ // Add detected stack summary
201
+ const stackParts: string[] = [];
202
+ if (stack.framework) {
203
+ stackParts.push(`${stack.framework.name}${stack.framework.router ? ` (${stack.framework.router} router)` : ""}`);
204
+ }
205
+ if (stack.orm) {
206
+ stackParts.push(stack.orm.name);
207
+ }
208
+ if (stack.auth) {
209
+ stackParts.push(stack.auth.name);
210
+ }
211
+ if (stack.payments) {
212
+ stackParts.push(stack.payments.name);
213
+ }
214
+
215
+ if (stackParts.length > 0) {
216
+ lines.push(`**Detected stack:** ${stackParts.join(" + ")}`);
217
+ lines.push("");
218
+ }
219
+
104
220
  for (const [category, invs] of grouped) {
105
221
  lines.push(`## ${formatCategoryName(category)}`);
106
222
  lines.push("");
107
223
 
108
224
  for (const inv of invs) {
109
- lines.push(`- [${inv.id}] ${inv.category} | ${inv.statement}`);
225
+ lines.push(`- [ ] **${inv.id}**: ${inv.statement}`);
110
226
  }
111
227
 
112
228
  lines.push("");