@bantay/cli 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/package.json +46 -0
- package/src/aide/index.ts +301 -0
- package/src/aide/types.ts +94 -0
- package/src/checkers/auth.ts +111 -0
- package/src/checkers/logging.ts +133 -0
- package/src/checkers/registry.ts +46 -0
- package/src/checkers/schema.ts +157 -0
- package/src/checkers/types.ts +30 -0
- package/src/cli.ts +314 -0
- package/src/commands/aide.ts +571 -0
- package/src/commands/check.ts +363 -0
- package/src/commands/init.ts +75 -0
- package/src/config.ts +63 -0
- package/src/detectors/index.ts +61 -0
- package/src/detectors/nextjs.ts +97 -0
- package/src/detectors/prisma.ts +80 -0
- package/src/detectors/types.ts +29 -0
- package/src/diff.ts +124 -0
- package/src/export/aide-reader.ts +112 -0
- package/src/export/all.ts +29 -0
- package/src/export/claude.ts +221 -0
- package/src/export/cursor.ts +69 -0
- package/src/export/index.ts +28 -0
- package/src/export/invariants.ts +92 -0
- package/src/export/types.ts +70 -0
- package/src/generators/config.ts +170 -0
- package/src/generators/invariants.ts +161 -0
- package/src/prerequisites.ts +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Zachary Cancio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Bantay
|
|
2
|
+
|
|
3
|
+
Write down the rules your system must never break. We enforce them on every PR.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx @bantay/cli init # Detect stack, generate invariants.md
|
|
9
|
+
bantay check # Verify all invariants
|
|
10
|
+
bantay export claude # Export to CLAUDE.md for agent context
|
|
11
|
+
bantay ci --github-actions # Generate CI workflow
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What invariants.md looks like
|
|
15
|
+
|
|
16
|
+
```markdown
|
|
17
|
+
## Auth
|
|
18
|
+
- [inv_auth_on_routes] auth | All API routes check authentication before processing
|
|
19
|
+
|
|
20
|
+
## Schema
|
|
21
|
+
- [inv_timestamps] schema | All tables have createdAt and updatedAt columns
|
|
22
|
+
|
|
23
|
+
## Logging
|
|
24
|
+
- [inv_no_pii_logs] logging | No PII (email, phone, SSN) appears in log output
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each invariant has a stable ID, category, and statement. `bantay check` evaluates them against your codebase using static analysis.
|
|
28
|
+
|
|
29
|
+
## Three-tier checkers
|
|
30
|
+
|
|
31
|
+
| Tier | Location | Example |
|
|
32
|
+
|------|----------|---------|
|
|
33
|
+
| **Built-in** | Ships with `@bantay/cli` | `auth-on-routes`, `timestamps-on-tables` |
|
|
34
|
+
| **Community** | npm packages | `@bantay/checker-stripe`, `@bantay/checker-posthog` |
|
|
35
|
+
| **Project** | `.bantay/checkers/*.ts` | Custom rules for your codebase |
|
|
36
|
+
|
|
37
|
+
All tiers implement the same interface. Resolution order: project > community > built-in.
|
|
38
|
+
|
|
39
|
+
## The .aide spec
|
|
40
|
+
|
|
41
|
+
Bantay uses a `.aide` file as its source of truth. `invariants.md`, `CLAUDE.md`, and `.cursorrules` are generated exports.
|
|
42
|
+
|
|
43
|
+
See [bantay.aide](./bantay.aide) for the living spec.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
bantay init Initialize in current project
|
|
49
|
+
bantay check Check all invariants
|
|
50
|
+
bantay check --diff HEAD~1 Check only affected invariants
|
|
51
|
+
bantay check --id inv_auth Check single invariant
|
|
52
|
+
bantay export invariants Generate invariants.md from .aide
|
|
53
|
+
bantay export claude Export to CLAUDE.md
|
|
54
|
+
bantay export cursor Export to .cursorrules
|
|
55
|
+
bantay export all Export all targets
|
|
56
|
+
bantay ci --github-actions Generate GitHub Actions workflow
|
|
57
|
+
bantay aide show View the .aide entity tree
|
|
58
|
+
bantay aide validate Validate .aide syntax
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bantay/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Write down the rules your system must never break. We enforce them on every PR.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bantay": "./src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"build": "bun build ./src/cli.ts --outdir ./dist --target bun",
|
|
17
|
+
"dev": "bun run ./src/cli.ts"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"invariants",
|
|
21
|
+
"cli",
|
|
22
|
+
"testing",
|
|
23
|
+
"ci",
|
|
24
|
+
"static-analysis",
|
|
25
|
+
"code-quality"
|
|
26
|
+
],
|
|
27
|
+
"author": "Zachary Cancio",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/zcancio/bantay.git"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"js-yaml": "^4.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "latest"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"typescript": "^5"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18",
|
|
44
|
+
"bun": ">=1.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import * as yaml from "js-yaml";
|
|
3
|
+
import {
|
|
4
|
+
type AideTree,
|
|
5
|
+
type Entity,
|
|
6
|
+
type Relationship,
|
|
7
|
+
type AddEntityOptions,
|
|
8
|
+
type RemoveEntityOptions,
|
|
9
|
+
type AddRelationshipOptions,
|
|
10
|
+
type RelationshipType,
|
|
11
|
+
VALID_RELATIONSHIP_TYPES,
|
|
12
|
+
ID_PREFIX_CONVENTIONS,
|
|
13
|
+
SCENARIO_PREFIX,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
// Re-export types
|
|
17
|
+
export type { AideTree, Entity, Relationship } from "./types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read and parse a .aide YAML file
|
|
21
|
+
*/
|
|
22
|
+
export async function read(path: string): Promise<AideTree> {
|
|
23
|
+
const content = await readFile(path, "utf-8");
|
|
24
|
+
const parsed = yaml.load(content) as {
|
|
25
|
+
entities?: Record<string, unknown>;
|
|
26
|
+
relationships?: unknown[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const entities: Record<string, Entity> = {};
|
|
30
|
+
const relationships: Relationship[] = [];
|
|
31
|
+
|
|
32
|
+
// Parse entities
|
|
33
|
+
if (parsed.entities) {
|
|
34
|
+
for (const [id, value] of Object.entries(parsed.entities)) {
|
|
35
|
+
const entity = value as Record<string, unknown>;
|
|
36
|
+
entities[id] = {
|
|
37
|
+
display: entity.display as string | undefined,
|
|
38
|
+
parent: entity.parent as string | undefined,
|
|
39
|
+
props: entity.props as Record<string, unknown> | undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parse relationships
|
|
45
|
+
if (parsed.relationships && Array.isArray(parsed.relationships)) {
|
|
46
|
+
for (const rel of parsed.relationships) {
|
|
47
|
+
const r = rel as Record<string, unknown>;
|
|
48
|
+
relationships.push({
|
|
49
|
+
from: r.from as string,
|
|
50
|
+
to: r.to as string,
|
|
51
|
+
type: r.type as RelationshipType,
|
|
52
|
+
cardinality: r.cardinality as string,
|
|
53
|
+
} as Relationship);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { entities, relationships };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write an aide tree to a .aide YAML file
|
|
62
|
+
*/
|
|
63
|
+
export async function write(path: string, tree: AideTree): Promise<void> {
|
|
64
|
+
const content = yaml.dump(
|
|
65
|
+
{
|
|
66
|
+
entities: tree.entities,
|
|
67
|
+
relationships: tree.relationships,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
indent: 2,
|
|
71
|
+
lineWidth: -1, // Don't wrap lines
|
|
72
|
+
noRefs: true,
|
|
73
|
+
sortKeys: false,
|
|
74
|
+
quotingType: '"',
|
|
75
|
+
forceQuotes: false,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
await writeFile(path, content, "utf-8");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add an entity to the tree
|
|
83
|
+
*/
|
|
84
|
+
export function addEntity(tree: AideTree, options: AddEntityOptions): AideTree {
|
|
85
|
+
const { parent, display, props } = options;
|
|
86
|
+
let { id } = options;
|
|
87
|
+
|
|
88
|
+
// Validate parent exists if specified
|
|
89
|
+
if (parent && !tree.entities[parent]) {
|
|
90
|
+
throw new Error(`Parent entity "${parent}" not found`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Auto-generate ID if not provided
|
|
94
|
+
if (!id) {
|
|
95
|
+
id = generateEntityId(tree, parent);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check for duplicate ID
|
|
99
|
+
if (tree.entities[id]) {
|
|
100
|
+
throw new Error(`Entity "${id}" already exists`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create new entity
|
|
104
|
+
const entity: Entity = {};
|
|
105
|
+
if (display) entity.display = display;
|
|
106
|
+
if (parent) entity.parent = parent;
|
|
107
|
+
if (props) entity.props = props;
|
|
108
|
+
|
|
109
|
+
// Return new tree with entity added
|
|
110
|
+
return {
|
|
111
|
+
entities: {
|
|
112
|
+
...tree.entities,
|
|
113
|
+
[id]: entity,
|
|
114
|
+
},
|
|
115
|
+
relationships: [...tree.relationships],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove an entity from the tree
|
|
121
|
+
*/
|
|
122
|
+
export function removeEntity(
|
|
123
|
+
tree: AideTree,
|
|
124
|
+
id: string,
|
|
125
|
+
options: RemoveEntityOptions = {}
|
|
126
|
+
): AideTree {
|
|
127
|
+
const { force = false } = options;
|
|
128
|
+
|
|
129
|
+
// Check entity exists
|
|
130
|
+
if (!tree.entities[id]) {
|
|
131
|
+
throw new Error(`Entity "${id}" not found`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Find relationships involving this entity
|
|
135
|
+
const involvedRelationships = tree.relationships.filter(
|
|
136
|
+
(r) => r.from === id || r.to === id
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (involvedRelationships.length > 0 && !force) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Cannot remove "${id}": relationships exist. Use force=true to remove anyway.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Find all child entities (cascade)
|
|
146
|
+
const idsToRemove = new Set<string>([id]);
|
|
147
|
+
findChildEntities(tree, id, idsToRemove);
|
|
148
|
+
|
|
149
|
+
// Remove entities
|
|
150
|
+
const newEntities: Record<string, Entity> = {};
|
|
151
|
+
for (const [entityId, entity] of Object.entries(tree.entities)) {
|
|
152
|
+
if (!idsToRemove.has(entityId)) {
|
|
153
|
+
newEntities[entityId] = entity;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove relationships involving removed entities
|
|
158
|
+
const newRelationships = tree.relationships.filter(
|
|
159
|
+
(r) => !idsToRemove.has(r.from) && !idsToRemove.has(r.to)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
entities: newEntities,
|
|
164
|
+
relationships: newRelationships,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Add a relationship to the tree
|
|
170
|
+
*/
|
|
171
|
+
export function addRelationship(
|
|
172
|
+
tree: AideTree,
|
|
173
|
+
options: AddRelationshipOptions
|
|
174
|
+
): AideTree {
|
|
175
|
+
const { from, to, type, cardinality } = options;
|
|
176
|
+
|
|
177
|
+
// Validate 'from' entity exists
|
|
178
|
+
if (!tree.entities[from]) {
|
|
179
|
+
throw new Error(`"from" entity "${from}" not found`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate 'to' entity exists
|
|
183
|
+
if (!tree.entities[to]) {
|
|
184
|
+
throw new Error(`"to" entity "${to}" not found`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate relationship type
|
|
188
|
+
if (!VALID_RELATIONSHIP_TYPES.includes(type)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Invalid relationship type "${type}". Valid types: ${VALID_RELATIONSHIP_TYPES.join(", ")}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const relationship: Relationship = { from, to, type, cardinality };
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
entities: { ...tree.entities },
|
|
198
|
+
relationships: [...tree.relationships, relationship],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate the aide tree and return an array of error messages
|
|
204
|
+
*/
|
|
205
|
+
export function validate(tree: AideTree): string[] {
|
|
206
|
+
const errors: string[] = [];
|
|
207
|
+
|
|
208
|
+
// Check for orphaned relationships (from entity missing)
|
|
209
|
+
for (const rel of tree.relationships) {
|
|
210
|
+
if (!tree.entities[rel.from]) {
|
|
211
|
+
errors.push(
|
|
212
|
+
`Orphaned relationship: "from" entity "${rel.from}" not found`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!tree.entities[rel.to]) {
|
|
216
|
+
errors.push(`Orphaned relationship: "to" entity "${rel.to}" not found`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Validate relationship type
|
|
220
|
+
if (!VALID_RELATIONSHIP_TYPES.includes(rel.type)) {
|
|
221
|
+
errors.push(
|
|
222
|
+
`Invalid relationship type "${rel.type}" in relationship from "${rel.from}" to "${rel.to}"`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check for missing parent references
|
|
228
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
229
|
+
if (entity.parent && !tree.entities[entity.parent]) {
|
|
230
|
+
errors.push(`Entity "${id}" has missing parent "${entity.parent}"`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return errors;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Helper Functions ---
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find all child entities recursively
|
|
241
|
+
*/
|
|
242
|
+
function findChildEntities(
|
|
243
|
+
tree: AideTree,
|
|
244
|
+
parentId: string,
|
|
245
|
+
collected: Set<string>
|
|
246
|
+
): void {
|
|
247
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
248
|
+
if (entity.parent === parentId && !collected.has(id)) {
|
|
249
|
+
collected.add(id);
|
|
250
|
+
findChildEntities(tree, id, collected);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Generate an entity ID based on parent conventions
|
|
257
|
+
*/
|
|
258
|
+
function generateEntityId(tree: AideTree, parent?: string): string {
|
|
259
|
+
if (!parent) {
|
|
260
|
+
// Generate a generic ID
|
|
261
|
+
let counter = 1;
|
|
262
|
+
while (tree.entities[`entity_${counter}`]) {
|
|
263
|
+
counter++;
|
|
264
|
+
}
|
|
265
|
+
return `entity_${counter}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check if parent has a known prefix convention
|
|
269
|
+
let prefix = ID_PREFIX_CONVENTIONS[parent];
|
|
270
|
+
|
|
271
|
+
// If parent is a CUJ (starts with cuj_), generate scenario prefix
|
|
272
|
+
if (!prefix && parent.startsWith("cuj_")) {
|
|
273
|
+
prefix = SCENARIO_PREFIX;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If parent itself has a prefix, use the same prefix
|
|
277
|
+
if (!prefix) {
|
|
278
|
+
for (const [container, containerPrefix] of Object.entries(
|
|
279
|
+
ID_PREFIX_CONVENTIONS
|
|
280
|
+
)) {
|
|
281
|
+
if (tree.entities[parent]?.parent === container) {
|
|
282
|
+
prefix = containerPrefix;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Default to a generic prefix based on parent
|
|
289
|
+
if (!prefix) {
|
|
290
|
+
prefix = `${parent}_`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Find next available number for this prefix
|
|
294
|
+
let counter = 1;
|
|
295
|
+
while (tree.entities[`${prefix}${counter}`]) {
|
|
296
|
+
counter++;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return `${prefix}${counter}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Valid relationship types in an aide file
|
|
3
|
+
*/
|
|
4
|
+
export const VALID_RELATIONSHIP_TYPES = [
|
|
5
|
+
"protected_by",
|
|
6
|
+
"depends_on",
|
|
7
|
+
"implements",
|
|
8
|
+
"delegates_to",
|
|
9
|
+
"weakens",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type RelationshipType = (typeof VALID_RELATIONSHIP_TYPES)[number];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Valid cardinality values
|
|
16
|
+
*/
|
|
17
|
+
export const VALID_CARDINALITIES = [
|
|
18
|
+
"one_to_one",
|
|
19
|
+
"one_to_many",
|
|
20
|
+
"many_to_one",
|
|
21
|
+
"many_to_many",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type Cardinality = (typeof VALID_CARDINALITIES)[number];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A relationship between two entities in the aide tree
|
|
28
|
+
*/
|
|
29
|
+
export interface Relationship {
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
type: RelationshipType;
|
|
33
|
+
cardinality: Cardinality;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An entity in the aide tree
|
|
38
|
+
*/
|
|
39
|
+
export interface Entity {
|
|
40
|
+
display?: string;
|
|
41
|
+
parent?: string;
|
|
42
|
+
props?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The complete aide tree structure
|
|
47
|
+
*/
|
|
48
|
+
export interface AideTree {
|
|
49
|
+
entities: Record<string, Entity>;
|
|
50
|
+
relationships: Relationship[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Options for adding an entity
|
|
55
|
+
*/
|
|
56
|
+
export interface AddEntityOptions {
|
|
57
|
+
id?: string;
|
|
58
|
+
parent?: string;
|
|
59
|
+
display?: string;
|
|
60
|
+
props?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for removing an entity
|
|
65
|
+
*/
|
|
66
|
+
export interface RemoveEntityOptions {
|
|
67
|
+
force?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Options for adding a relationship
|
|
72
|
+
*/
|
|
73
|
+
export interface AddRelationshipOptions {
|
|
74
|
+
from: string;
|
|
75
|
+
to: string;
|
|
76
|
+
type: RelationshipType;
|
|
77
|
+
cardinality: Cardinality;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Prefix conventions for auto-generating entity IDs based on parent
|
|
82
|
+
*/
|
|
83
|
+
export const ID_PREFIX_CONVENTIONS: Record<string, string> = {
|
|
84
|
+
cujs: "cuj_",
|
|
85
|
+
invariants: "inv_",
|
|
86
|
+
constraints: "con_",
|
|
87
|
+
foundations: "found_",
|
|
88
|
+
wisdom: "wis_",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prefix for entities under a CUJ (scenarios)
|
|
93
|
+
*/
|
|
94
|
+
export const SCENARIO_PREFIX = "sc_";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Checker, CheckResult, CheckerContext, CheckViolation } from "./types";
|
|
2
|
+
import type { Invariant } from "../generators/invariants";
|
|
3
|
+
import { Glob } from "bun";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { join, relative } from "path";
|
|
6
|
+
|
|
7
|
+
// Auth patterns to look for in route files
|
|
8
|
+
const AUTH_PATTERNS = [
|
|
9
|
+
// Next.js auth patterns
|
|
10
|
+
/auth\(\)/,
|
|
11
|
+
/getServerSession/,
|
|
12
|
+
/useSession/,
|
|
13
|
+
/withAuth/,
|
|
14
|
+
/requireAuth/,
|
|
15
|
+
/checkAuth/,
|
|
16
|
+
/isAuthenticated/,
|
|
17
|
+
/currentUser/,
|
|
18
|
+
/getUser/,
|
|
19
|
+
// Clerk patterns
|
|
20
|
+
/auth\(\)\.protect/,
|
|
21
|
+
/clerkMiddleware/,
|
|
22
|
+
/SignedIn/,
|
|
23
|
+
/SignedOut/,
|
|
24
|
+
// Auth.js / NextAuth patterns
|
|
25
|
+
/getSession/,
|
|
26
|
+
/unstable_getServerSession/,
|
|
27
|
+
// Supabase patterns
|
|
28
|
+
/supabase\.auth/,
|
|
29
|
+
/createServerClient/,
|
|
30
|
+
// Generic patterns
|
|
31
|
+
/middleware.*auth/i,
|
|
32
|
+
/protected/i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Route handler exports that need auth
|
|
36
|
+
const ROUTE_EXPORTS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
37
|
+
|
|
38
|
+
async function findRouteFiles(
|
|
39
|
+
projectPath: string,
|
|
40
|
+
routeDirectories: string[]
|
|
41
|
+
): Promise<string[]> {
|
|
42
|
+
const routeFiles: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const routeDir of routeDirectories) {
|
|
45
|
+
const pattern = "**/{route,page}.{ts,tsx,js,jsx}";
|
|
46
|
+
const glob = new Glob(pattern);
|
|
47
|
+
|
|
48
|
+
const dirPath = join(projectPath, routeDir);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
for await (const file of glob.scan({ cwd: dirPath, absolute: true })) {
|
|
52
|
+
routeFiles.push(file);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Directory doesn't exist, skip
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return routeFiles;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasAuthCheck(content: string): boolean {
|
|
63
|
+
return AUTH_PATTERNS.some((pattern) => pattern.test(content));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isApiRoute(content: string): boolean {
|
|
67
|
+
// Check if file exports HTTP method handlers
|
|
68
|
+
return ROUTE_EXPORTS.some((method) => {
|
|
69
|
+
const pattern = new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`);
|
|
70
|
+
return pattern.test(content);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const authChecker: Checker = {
|
|
75
|
+
category: "auth",
|
|
76
|
+
|
|
77
|
+
async check(invariant: Invariant, context: CheckerContext): Promise<CheckResult> {
|
|
78
|
+
const violations: CheckViolation[] = [];
|
|
79
|
+
const routeDirs = context.config.routeDirectories ?? ["app/api", "pages/api"];
|
|
80
|
+
|
|
81
|
+
const routeFiles = await findRouteFiles(context.projectPath, routeDirs);
|
|
82
|
+
|
|
83
|
+
for (const filePath of routeFiles) {
|
|
84
|
+
try {
|
|
85
|
+
const content = await readFile(filePath, "utf-8");
|
|
86
|
+
|
|
87
|
+
// Only check API routes (files with HTTP method exports)
|
|
88
|
+
if (!isApiRoute(content)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if any auth pattern is present
|
|
93
|
+
if (!hasAuthCheck(content)) {
|
|
94
|
+
violations.push({
|
|
95
|
+
filePath: relative(context.projectPath, filePath),
|
|
96
|
+
line: 1,
|
|
97
|
+
message: "API route missing authentication check",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// File read error, skip
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
invariant,
|
|
107
|
+
status: violations.length > 0 ? "fail" : "pass",
|
|
108
|
+
violations,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|