@bantay/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.ts +95 -9
- package/src/commands/ci.ts +228 -0
- package/src/commands/status.ts +309 -0
- package/src/detectors/authjs.ts +92 -0
- package/src/detectors/clerk.ts +88 -0
- package/src/detectors/drizzle.ts +105 -0
- package/src/detectors/index.ts +35 -14
- package/src/detectors/nextjs.ts +13 -3
- package/src/detectors/stripe.ts +88 -0
- package/src/detectors/types.ts +12 -0
- package/src/export/all.ts +5 -1
- package/src/export/codex.ts +70 -0
- package/src/export/index.ts +1 -0
- package/src/generators/invariants.ts +170 -54
|
@@ -12,62 +12,159 @@ interface InvariantTemplate {
|
|
|
12
12
|
statement: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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[] = [
|
|
20
|
+
const invariants: InvariantTemplate[] = [];
|
|
64
21
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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(`- [
|
|
225
|
+
lines.push(`- [ ] **${inv.id}**: ${inv.statement}`);
|
|
110
226
|
}
|
|
111
227
|
|
|
112
228
|
lines.push("");
|