@everystack/mcp 0.3.2 → 0.4.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/dist/adding-database.md +36 -23
- package/dist/cli.md +44 -3
- package/dist/core.md +19 -1
- package/dist/database-operations.md +236 -0
- package/dist/derived-objects.md +225 -0
- package/dist/gates/detectors/embedded-data-bundle.d.ts +13 -0
- package/dist/gates/detectors/embedded-data-bundle.d.ts.map +1 -0
- package/dist/gates/detectors/embedded-data-bundle.js +56 -0
- package/dist/gates/detectors/hand-written-migration.d.ts +24 -0
- package/dist/gates/detectors/hand-written-migration.d.ts.map +1 -0
- package/dist/gates/detectors/hand-written-migration.js +54 -0
- package/dist/gates/detectors/secret-in-public-env.d.ts +12 -0
- package/dist/gates/detectors/secret-in-public-env.d.ts.map +1 -0
- package/dist/gates/detectors/secret-in-public-env.js +38 -0
- package/dist/gates/engine.d.ts +28 -0
- package/dist/gates/engine.d.ts.map +1 -0
- package/dist/gates/engine.js +74 -0
- package/dist/gates/registry.d.ts +14 -0
- package/dist/gates/registry.d.ts.map +1 -0
- package/dist/gates/registry.js +21 -0
- package/dist/gates/telemetry.d.ts +47 -0
- package/dist/gates/telemetry.d.ts.map +1 -0
- package/dist/gates/telemetry.js +121 -0
- package/dist/gates/types.d.ts +67 -0
- package/dist/gates/types.d.ts.map +1 -0
- package/dist/gates/types.js +14 -0
- package/dist/governance/cli.d.ts +57 -0
- package/dist/governance/cli.d.ts.map +1 -0
- package/dist/governance/cli.js +169 -0
- package/dist/governance/grounding.d.ts +78 -0
- package/dist/governance/grounding.d.ts.map +1 -0
- package/dist/governance/grounding.js +299 -0
- package/dist/index.cjs +119 -107
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/project-claude-md.md +35 -18
- package/dist/prompts/add-feature.d.ts +3 -0
- package/dist/prompts/add-feature.d.ts.map +1 -0
- package/dist/prompts/add-feature.js +154 -0
- package/dist/prompts/claude-md.d.ts +12 -0
- package/dist/prompts/claude-md.d.ts.map +1 -0
- package/dist/prompts/claude-md.js +87 -0
- package/dist/prompts/debug.d.ts +3 -0
- package/dist/prompts/debug.d.ts.map +1 -0
- package/dist/prompts/debug.js +129 -0
- package/dist/prompts/deploy.d.ts +3 -0
- package/dist/prompts/deploy.d.ts.map +1 -0
- package/dist/prompts/deploy.js +118 -0
- package/dist/prompts/design-schema.d.ts +3 -0
- package/dist/prompts/design-schema.d.ts.map +1 -0
- package/dist/prompts/design-schema.js +97 -0
- package/dist/prompts/governance-setup.d.ts +9 -0
- package/dist/prompts/governance-setup.d.ts.map +1 -0
- package/dist/prompts/governance-setup.js +76 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +20 -0
- package/dist/prompts/new-app.d.ts +3 -0
- package/dist/prompts/new-app.d.ts.map +1 -0
- package/dist/prompts/new-app.js +203 -0
- package/dist/prompts/runbook.d.ts +12 -0
- package/dist/prompts/runbook.d.ts.map +1 -0
- package/dist/prompts/runbook.js +70 -0
- package/dist/prompts/secure.d.ts +3 -0
- package/dist/prompts/secure.d.ts.map +1 -0
- package/dist/prompts/secure.js +219 -0
- package/dist/resources/index.d.ts +12 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +182 -0
- package/dist/schema-patterns.md +92 -106
- package/dist/tools/check-environment.d.ts +25 -0
- package/dist/tools/check-environment.d.ts.map +1 -0
- package/dist/tools/check-environment.js +281 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/project-status.d.ts +28 -0
- package/dist/tools/project-status.d.ts.map +1 -0
- package/dist/tools/project-status.js +138 -0
- package/dist/tools/project-validate.d.ts +19 -0
- package/dist/tools/project-validate.d.ts.map +1 -0
- package/dist/tools/project-validate.js +323 -0
- package/dist/tools/schema-analyze.d.ts +46 -0
- package/dist/tools/schema-analyze.d.ts.map +1 -0
- package/dist/tools/schema-analyze.js +336 -0
- package/package.json +3 -3
- package/src/gates/detectors/hand-written-migration.ts +29 -13
- package/src/index.ts +1 -1
- package/src/prompts/add-feature.ts +4 -4
- package/src/prompts/claude-md.ts +4 -2
- package/src/prompts/debug.ts +3 -4
- package/src/prompts/deploy.ts +17 -8
- package/src/prompts/design-schema.ts +45 -59
- package/src/prompts/new-app.ts +21 -19
- package/src/prompts/secure.ts +3 -3
- package/src/resources/adding-database.md +36 -23
- package/src/resources/cli.md +44 -3
- package/src/resources/core.md +19 -1
- package/src/resources/database-operations.md +236 -0
- package/src/resources/derived-objects.md +225 -0
- package/src/resources/index.ts +15 -1
- package/src/resources/project-claude-md.md +35 -18
- package/src/resources/schema-patterns.md +92 -106
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const V1_PACKAGES = ['@everystack/server', '@everystack/cli', '@everystack/ui'];
|
|
4
|
+
const V2_PACKAGES = ['@everystack/api', '@everystack/auth', '@everystack/admin', '@everystack/logging', '@everystack/security', '@everystack/query'];
|
|
5
|
+
const V3_PACKAGES = ['@everystack/jobs', '@everystack/storage', '@everystack/images'];
|
|
6
|
+
function readJson(path) {
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function detectTier(installed) {
|
|
17
|
+
const hasV3 = V3_PACKAGES.some((p) => installed.includes(p));
|
|
18
|
+
const hasV2 = V2_PACKAGES.some((p) => installed.includes(p));
|
|
19
|
+
const hasV1 = V1_PACKAGES.some((p) => installed.includes(p));
|
|
20
|
+
if (hasV3) {
|
|
21
|
+
return { tier: 'V3', explanation: `Full platform — has V3 packages (${V3_PACKAGES.filter((p) => installed.includes(p)).join(', ')})` };
|
|
22
|
+
}
|
|
23
|
+
if (hasV2) {
|
|
24
|
+
return { tier: 'V2', explanation: `Dynamic — has V2 packages (${V2_PACKAGES.filter((p) => installed.includes(p)).join(', ')})` };
|
|
25
|
+
}
|
|
26
|
+
if (hasV1) {
|
|
27
|
+
return { tier: 'V1', explanation: `Static — has V1 packages (${V1_PACKAGES.filter((p) => installed.includes(p)).join(', ')})` };
|
|
28
|
+
}
|
|
29
|
+
return { tier: 'unknown', explanation: 'No everystack packages found in dependencies' };
|
|
30
|
+
}
|
|
31
|
+
export function analyzeProjectStatus(projectPath) {
|
|
32
|
+
const warnings = [];
|
|
33
|
+
// Read package.json
|
|
34
|
+
const pkg = readJson(join(projectPath, 'package.json'));
|
|
35
|
+
if (!pkg) {
|
|
36
|
+
return {
|
|
37
|
+
tier: 'unknown',
|
|
38
|
+
tierExplanation: 'No package.json found at project path',
|
|
39
|
+
packages: { installed: [], missing: [] },
|
|
40
|
+
files: {
|
|
41
|
+
sstConfig: false, schemaDir: false, schemaFiles: [],
|
|
42
|
+
migrationsDir: false, serverDir: false, handlerFile: null,
|
|
43
|
+
workerFile: null, imageFile: null, seedFile: null,
|
|
44
|
+
},
|
|
45
|
+
deployment: { sstOutputs: false, stage: null, secrets: [] },
|
|
46
|
+
warnings: [
|
|
47
|
+
'No package.json found — is this the correct project path?',
|
|
48
|
+
'No project here yet? Describe what you want to build and Claude will help create one.',
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Collect everystack dependencies
|
|
53
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
54
|
+
const installed = Object.keys(allDeps).filter((d) => d.startsWith('@everystack/'));
|
|
55
|
+
const allKnown = [...V1_PACKAGES, ...V2_PACKAGES, ...V3_PACKAGES];
|
|
56
|
+
const missing = allKnown.filter((p) => !installed.includes(p));
|
|
57
|
+
// Detect tier
|
|
58
|
+
const { tier, explanation } = detectTier(installed);
|
|
59
|
+
// Check key files/directories
|
|
60
|
+
const sstConfig = existsSync(join(projectPath, 'sst.config.ts'));
|
|
61
|
+
const schemaDir = existsSync(join(projectPath, 'db'));
|
|
62
|
+
const migrationsDir = existsSync(join(projectPath, 'db', 'migrations')) ||
|
|
63
|
+
existsSync(join(projectPath, 'server', 'drizzle'));
|
|
64
|
+
const serverDir = existsSync(join(projectPath, 'server'));
|
|
65
|
+
// Find schema files
|
|
66
|
+
const schemaFiles = [];
|
|
67
|
+
if (schemaDir) {
|
|
68
|
+
const schemaPath = join(projectPath, 'db', 'schema.ts');
|
|
69
|
+
if (existsSync(schemaPath))
|
|
70
|
+
schemaFiles.push('db/schema.ts');
|
|
71
|
+
const indexPath = join(projectPath, 'db', 'index.ts');
|
|
72
|
+
if (existsSync(indexPath))
|
|
73
|
+
schemaFiles.push('db/index.ts');
|
|
74
|
+
}
|
|
75
|
+
// Check handler files
|
|
76
|
+
const handlerCandidates = ['server/api.ts', 'server/handler.ts', 'server/index.ts'];
|
|
77
|
+
const handlerFile = handlerCandidates.find((f) => existsSync(join(projectPath, f))) ?? null;
|
|
78
|
+
const workerFile = existsSync(join(projectPath, 'server', 'worker.ts')) ? 'server/worker.ts' : null;
|
|
79
|
+
const imageFile = existsSync(join(projectPath, 'server', 'image.ts')) ? 'server/image.ts' : null;
|
|
80
|
+
const seedFile = existsSync(join(projectPath, 'db', 'seed.ts')) ? 'db/seed.ts' : null;
|
|
81
|
+
// Check deployment state
|
|
82
|
+
const sstOutputsPath = join(projectPath, '.sst', 'outputs.json');
|
|
83
|
+
const sstOutputs = existsSync(sstOutputsPath);
|
|
84
|
+
let stage = null;
|
|
85
|
+
const secrets = [];
|
|
86
|
+
if (sstOutputs) {
|
|
87
|
+
const outputs = readJson(sstOutputsPath);
|
|
88
|
+
if (outputs) {
|
|
89
|
+
// SST outputs are keyed by stage
|
|
90
|
+
const keys = Object.keys(outputs);
|
|
91
|
+
if (keys.length > 0)
|
|
92
|
+
stage = keys[0];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Check for sst secret files
|
|
96
|
+
const sstDir = join(projectPath, '.sst');
|
|
97
|
+
if (existsSync(sstDir)) {
|
|
98
|
+
const secretCandidates = ['JwtSecret', 'DatabaseUrl', 'GoogleClientId', 'GoogleClientSecret', 'AppleClientId'];
|
|
99
|
+
for (const s of secretCandidates) {
|
|
100
|
+
if (existsSync(join(sstDir, 'secrets', s))) {
|
|
101
|
+
secrets.push(s);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Warnings
|
|
106
|
+
if (tier !== 'unknown' && tier !== 'V1' && !schemaDir) {
|
|
107
|
+
warnings.push('V2+ project but no db/ directory found — schema is required for PostgREST');
|
|
108
|
+
}
|
|
109
|
+
if (tier !== 'unknown' && tier !== 'V1' && schemaFiles.length === 0) {
|
|
110
|
+
warnings.push('No schema.ts found in db/ — create your Drizzle schema');
|
|
111
|
+
}
|
|
112
|
+
if (tier !== 'unknown' && !sstConfig) {
|
|
113
|
+
warnings.push('No sst.config.ts found — required for deployment');
|
|
114
|
+
}
|
|
115
|
+
if (installed.includes('@everystack/api') && !handlerFile) {
|
|
116
|
+
warnings.push('@everystack/api installed but no handler file found in server/');
|
|
117
|
+
}
|
|
118
|
+
if (installed.includes('@everystack/jobs') && !workerFile) {
|
|
119
|
+
warnings.push('@everystack/jobs installed but no server/worker.ts found');
|
|
120
|
+
}
|
|
121
|
+
if (installed.includes('@everystack/images') && !imageFile) {
|
|
122
|
+
warnings.push('@everystack/images installed but no server/image.ts found');
|
|
123
|
+
}
|
|
124
|
+
if (tier !== 'unknown' && tier !== 'V1' && !seedFile) {
|
|
125
|
+
warnings.push('No db/seed.ts found — recommended for development');
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
tier,
|
|
129
|
+
tierExplanation: explanation,
|
|
130
|
+
packages: { installed, missing },
|
|
131
|
+
files: {
|
|
132
|
+
sstConfig, schemaDir, schemaFiles, migrationsDir,
|
|
133
|
+
serverDir, handlerFile, workerFile, imageFile, seedFile,
|
|
134
|
+
},
|
|
135
|
+
deployment: { sstOutputs, stage, secrets },
|
|
136
|
+
warnings,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Severity = 'error' | 'warning' | 'info';
|
|
2
|
+
interface ValidationResult {
|
|
3
|
+
severity: Severity;
|
|
4
|
+
category: string;
|
|
5
|
+
message: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
fix?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ValidationReport {
|
|
10
|
+
results: ValidationResult[];
|
|
11
|
+
summary: {
|
|
12
|
+
errors: number;
|
|
13
|
+
warnings: number;
|
|
14
|
+
info: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function validateProject(projectPath: string): ValidationReport;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=project-validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-validate.d.ts","sourceRoot":"","sources":["../../src/tools/project-validate.ts"],"names":[],"mappings":"AAGA,KAAK,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7C,UAAU,gBAAgB;IACxB,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,UAAU,gBAAgB;IACxB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAqUD,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAuBrE"}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function readFileSafe(path) {
|
|
4
|
+
if (!existsSync(path))
|
|
5
|
+
return null;
|
|
6
|
+
try {
|
|
7
|
+
return readFileSync(path, 'utf-8');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function readJsonSafe(path) {
|
|
14
|
+
const content = readFileSafe(path);
|
|
15
|
+
if (!content)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function checkPackageJson(projectPath, results) {
|
|
25
|
+
const pkg = readJsonSafe(join(projectPath, 'package.json'));
|
|
26
|
+
if (!pkg) {
|
|
27
|
+
results.push({
|
|
28
|
+
severity: 'error',
|
|
29
|
+
category: 'project',
|
|
30
|
+
message: 'No package.json found',
|
|
31
|
+
fix: 'Run `npm init` or create package.json manually',
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const deps = {
|
|
36
|
+
...(pkg.dependencies || {}),
|
|
37
|
+
...(pkg.devDependencies || {}),
|
|
38
|
+
};
|
|
39
|
+
// Check for required peer dependencies
|
|
40
|
+
if (deps['@everystack/api'] && !deps['drizzle-orm']) {
|
|
41
|
+
results.push({
|
|
42
|
+
severity: 'error',
|
|
43
|
+
category: 'dependencies',
|
|
44
|
+
message: '@everystack/api requires drizzle-orm as a peer dependency',
|
|
45
|
+
fix: 'Run `pnpm add drizzle-orm drizzle-kit`',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (deps['@everystack/server'] && !deps['sst']) {
|
|
49
|
+
results.push({
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
category: 'dependencies',
|
|
52
|
+
message: '@everystack/server expects sst as a peer dependency for deployment',
|
|
53
|
+
fix: 'Run `pnpm add -D sst`',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (deps['@everystack/query'] && !deps['@tanstack/react-query']) {
|
|
57
|
+
results.push({
|
|
58
|
+
severity: 'error',
|
|
59
|
+
category: 'dependencies',
|
|
60
|
+
message: '@everystack/query requires @tanstack/react-query as a peer dependency',
|
|
61
|
+
fix: 'Run `pnpm add @tanstack/react-query`',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function checkSecurityConfig(projectPath, results) {
|
|
66
|
+
// Check for exposed secrets in common locations
|
|
67
|
+
const dangerousFiles = ['.env', '.env.local', '.env.production'];
|
|
68
|
+
for (const f of dangerousFiles) {
|
|
69
|
+
const content = readFileSafe(join(projectPath, f));
|
|
70
|
+
if (content) {
|
|
71
|
+
if (content.includes('JWT_SECRET') || content.includes('DATABASE_URL')) {
|
|
72
|
+
results.push({
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
category: 'security',
|
|
75
|
+
message: `${f} contains sensitive values — use SST secrets instead`,
|
|
76
|
+
file: f,
|
|
77
|
+
fix: 'Run `pnpm sst secret set JwtSecret "value" --stage dev` and remove from .env',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Check gitignore
|
|
81
|
+
const gitignore = readFileSafe(join(projectPath, '.gitignore'));
|
|
82
|
+
if (gitignore && !gitignore.includes(f)) {
|
|
83
|
+
results.push({
|
|
84
|
+
severity: 'error',
|
|
85
|
+
category: 'security',
|
|
86
|
+
message: `${f} is not in .gitignore — secrets may be committed`,
|
|
87
|
+
file: '.gitignore',
|
|
88
|
+
fix: `Add '${f}' to .gitignore`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Check for hardcoded secrets in handler files
|
|
94
|
+
const serverFiles = ['server/api.ts', 'server/handler.ts', 'server/plugins/api.ts'];
|
|
95
|
+
for (const f of serverFiles) {
|
|
96
|
+
const content = readFileSafe(join(projectPath, f));
|
|
97
|
+
if (content) {
|
|
98
|
+
if (/['"](?:sk_|pk_|secret_)[a-zA-Z0-9]+['"]/.test(content)) {
|
|
99
|
+
results.push({
|
|
100
|
+
severity: 'error',
|
|
101
|
+
category: 'security',
|
|
102
|
+
message: `Possible hardcoded API key in ${f}`,
|
|
103
|
+
file: f,
|
|
104
|
+
fix: 'Move secrets to SST secret management',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function checkHandlerConfig(projectPath, results) {
|
|
111
|
+
const handlerCandidates = [
|
|
112
|
+
'server/plugins/api.ts',
|
|
113
|
+
'server/api.ts',
|
|
114
|
+
'server/handler.ts',
|
|
115
|
+
];
|
|
116
|
+
let handlerSource = null;
|
|
117
|
+
let handlerFile = null;
|
|
118
|
+
for (const f of handlerCandidates) {
|
|
119
|
+
const content = readFileSafe(join(projectPath, f));
|
|
120
|
+
if (content && content.includes('createHandler')) {
|
|
121
|
+
handlerSource = content;
|
|
122
|
+
handlerFile = f;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!handlerSource || !handlerFile)
|
|
127
|
+
return;
|
|
128
|
+
// pgSettings is required for RLS
|
|
129
|
+
if (!handlerSource.includes('pgSettings')) {
|
|
130
|
+
results.push({
|
|
131
|
+
severity: 'warning',
|
|
132
|
+
category: 'security',
|
|
133
|
+
message: 'No pgSettings configured — RLS policies will not receive JWT claims',
|
|
134
|
+
file: handlerFile,
|
|
135
|
+
fix: 'Add pgSettings to your createHandler() config. Read everystack://security for the pattern.',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Check for auth configuration
|
|
139
|
+
if (!handlerSource.includes('verifyToken')) {
|
|
140
|
+
results.push({
|
|
141
|
+
severity: 'warning',
|
|
142
|
+
category: 'security',
|
|
143
|
+
message: 'No auth.verifyToken configured — all routes are unauthenticated',
|
|
144
|
+
file: handlerFile,
|
|
145
|
+
fix: 'Add auth.verifyToken to createHandler() config',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// exposedTables should be set
|
|
149
|
+
if (!handlerSource.includes('exposedTables')) {
|
|
150
|
+
results.push({
|
|
151
|
+
severity: 'warning',
|
|
152
|
+
category: 'security',
|
|
153
|
+
message: 'No exposedTables set — all schema tables are accessible via API',
|
|
154
|
+
file: handlerFile,
|
|
155
|
+
fix: 'Add exposedTables to restrict which tables are accessible',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// maxEmbedDepth should be set
|
|
159
|
+
if (!handlerSource.includes('maxEmbedDepth')) {
|
|
160
|
+
results.push({
|
|
161
|
+
severity: 'info',
|
|
162
|
+
category: 'security',
|
|
163
|
+
message: 'maxEmbedDepth not set — using default (3). Set explicitly to prevent deep query DoS.',
|
|
164
|
+
file: handlerFile,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// rowOwnership for user-scoped tables
|
|
168
|
+
if (!handlerSource.includes('rowOwnership')) {
|
|
169
|
+
results.push({
|
|
170
|
+
severity: 'info',
|
|
171
|
+
category: 'security',
|
|
172
|
+
message: 'No rowOwnership configured — consider it for user-scoped mutation control',
|
|
173
|
+
file: handlerFile,
|
|
174
|
+
fix: 'Add rowOwnership for tables where users should only modify their own rows',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function checkSchemaConventions(projectPath, results) {
|
|
179
|
+
const schemaPath = join(projectPath, 'db', 'schema.ts');
|
|
180
|
+
const source = readFileSafe(schemaPath);
|
|
181
|
+
if (!source)
|
|
182
|
+
return;
|
|
183
|
+
// Check for UUID primary keys (recommended)
|
|
184
|
+
const serialPkRegex = /serial\(['"]id['"]\)\.primaryKey/;
|
|
185
|
+
if (serialPkRegex.test(source)) {
|
|
186
|
+
results.push({
|
|
187
|
+
severity: 'info',
|
|
188
|
+
category: 'conventions',
|
|
189
|
+
message: 'Using serial IDs — UUID primary keys are recommended for distributed systems',
|
|
190
|
+
file: 'db/schema.ts',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Check for timestamps
|
|
194
|
+
const tableNames = [...source.matchAll(/export\s+const\s+(\w+)\s*=\s*pgTable/g)].map((m) => m[1]);
|
|
195
|
+
for (const name of tableNames) {
|
|
196
|
+
// Find the table definition block
|
|
197
|
+
const tableRegex = new RegExp(`export\\s+const\\s+${name}\\s*=\\s*pgTable\\([^,]+,\\s*\\{([^}]+)\\}`);
|
|
198
|
+
const match = source.match(tableRegex);
|
|
199
|
+
if (match) {
|
|
200
|
+
const block = match[1];
|
|
201
|
+
if (!block.includes('created_at') && !block.includes('createdAt')) {
|
|
202
|
+
results.push({
|
|
203
|
+
severity: 'info',
|
|
204
|
+
category: 'conventions',
|
|
205
|
+
message: `Table '${name}' has no createdAt timestamp`,
|
|
206
|
+
file: 'db/schema.ts',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function checkMigrations(projectPath, results) {
|
|
213
|
+
const migrationsDir = join(projectPath, 'db', 'migrations');
|
|
214
|
+
const serverDrizzle = join(projectPath, 'server', 'drizzle');
|
|
215
|
+
const hasDbMigrations = existsSync(migrationsDir);
|
|
216
|
+
const hasServerDrizzle = existsSync(serverDrizzle);
|
|
217
|
+
if (!hasDbMigrations && !hasServerDrizzle) {
|
|
218
|
+
const hasSchema = existsSync(join(projectPath, 'db', 'schema.ts'));
|
|
219
|
+
if (hasSchema) {
|
|
220
|
+
results.push({
|
|
221
|
+
severity: 'warning',
|
|
222
|
+
category: 'database',
|
|
223
|
+
message: 'Schema exists but no migrations directory found',
|
|
224
|
+
fix: 'Run `npx drizzle-kit generate` to create migrations',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Check for RLS in migrations
|
|
230
|
+
const dir = hasServerDrizzle ? serverDrizzle : migrationsDir;
|
|
231
|
+
try {
|
|
232
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.sql'));
|
|
233
|
+
let hasRls = false;
|
|
234
|
+
for (const f of files) {
|
|
235
|
+
const content = readFileSafe(join(dir, f));
|
|
236
|
+
if (content && content.includes('ENABLE ROW LEVEL SECURITY')) {
|
|
237
|
+
hasRls = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!hasRls && files.length > 0) {
|
|
242
|
+
results.push({
|
|
243
|
+
severity: 'warning',
|
|
244
|
+
category: 'security',
|
|
245
|
+
message: 'No RLS policies found in migrations — tables are not protected at database level',
|
|
246
|
+
fix: 'Add RLS policies via custom SQL migrations. Read everystack://security for templates.',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function checkDeploymentReadiness(projectPath, results) {
|
|
255
|
+
// SST config
|
|
256
|
+
if (!existsSync(join(projectPath, 'sst.config.ts'))) {
|
|
257
|
+
results.push({
|
|
258
|
+
severity: 'info',
|
|
259
|
+
category: 'deployment',
|
|
260
|
+
message: 'No sst.config.ts — project is not configured for AWS deployment',
|
|
261
|
+
fix: 'Create sst.config.ts following everystack://deployment',
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const sstConfig = readFileSafe(join(projectPath, 'sst.config.ts'));
|
|
266
|
+
if (sstConfig) {
|
|
267
|
+
// Check for VPC configuration
|
|
268
|
+
if (!sstConfig.includes('Vpc') && !sstConfig.includes('vpc')) {
|
|
269
|
+
results.push({
|
|
270
|
+
severity: 'info',
|
|
271
|
+
category: 'deployment',
|
|
272
|
+
message: 'No VPC configured in sst.config.ts — RDS requires a VPC',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function checkFileConventions(projectPath, results) {
|
|
278
|
+
// Check for default exports (everystack uses named exports)
|
|
279
|
+
const srcDirs = ['server', 'db', 'lib'];
|
|
280
|
+
for (const dir of srcDirs) {
|
|
281
|
+
const dirPath = join(projectPath, dir);
|
|
282
|
+
if (!existsSync(dirPath))
|
|
283
|
+
continue;
|
|
284
|
+
try {
|
|
285
|
+
const files = readdirSync(dirPath).filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts'));
|
|
286
|
+
for (const f of files) {
|
|
287
|
+
const content = readFileSafe(join(dirPath, f));
|
|
288
|
+
if (content && /^export\s+default\s/m.test(content)) {
|
|
289
|
+
results.push({
|
|
290
|
+
severity: 'info',
|
|
291
|
+
category: 'conventions',
|
|
292
|
+
message: `${dir}/${f} uses default export — everystack convention prefers named exports`,
|
|
293
|
+
file: `${dir}/${f}`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// ignore
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export function validateProject(projectPath) {
|
|
304
|
+
const results = [];
|
|
305
|
+
checkPackageJson(projectPath, results);
|
|
306
|
+
checkSecurityConfig(projectPath, results);
|
|
307
|
+
checkHandlerConfig(projectPath, results);
|
|
308
|
+
checkSchemaConventions(projectPath, results);
|
|
309
|
+
checkMigrations(projectPath, results);
|
|
310
|
+
checkDeploymentReadiness(projectPath, results);
|
|
311
|
+
checkFileConventions(projectPath, results);
|
|
312
|
+
// Sort: errors first, then warnings, then info
|
|
313
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
314
|
+
results.sort((a, b) => order[a.severity] - order[b.severity]);
|
|
315
|
+
return {
|
|
316
|
+
results,
|
|
317
|
+
summary: {
|
|
318
|
+
errors: results.filter((r) => r.severity === 'error').length,
|
|
319
|
+
warnings: results.filter((r) => r.severity === 'warning').length,
|
|
320
|
+
info: results.filter((r) => r.severity === 'info').length,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface ColumnInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
dbName: string;
|
|
4
|
+
type: string;
|
|
5
|
+
modifiers: string[];
|
|
6
|
+
}
|
|
7
|
+
interface TableInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
exportName: string;
|
|
10
|
+
columns: ColumnInfo[];
|
|
11
|
+
file: string;
|
|
12
|
+
}
|
|
13
|
+
interface RelationInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
table: string;
|
|
16
|
+
type: 'one' | 'many';
|
|
17
|
+
fields: string[];
|
|
18
|
+
references: string[];
|
|
19
|
+
relationName?: string;
|
|
20
|
+
}
|
|
21
|
+
interface HandlerConfig {
|
|
22
|
+
exposedTables: string[];
|
|
23
|
+
hiddenColumns: Record<string, string[]>;
|
|
24
|
+
protectedFields: Record<string, string[]>;
|
|
25
|
+
rowOwnership: Record<string, {
|
|
26
|
+
column: string;
|
|
27
|
+
userField: string;
|
|
28
|
+
}>;
|
|
29
|
+
softDelete: {
|
|
30
|
+
tables: string[];
|
|
31
|
+
};
|
|
32
|
+
pgSettings: boolean;
|
|
33
|
+
relations: Record<string, Record<string, unknown>>;
|
|
34
|
+
publicRoutes: string[];
|
|
35
|
+
publicRpc: string[];
|
|
36
|
+
}
|
|
37
|
+
interface SchemaAnalysis {
|
|
38
|
+
tables: TableInfo[];
|
|
39
|
+
relations: RelationInfo[];
|
|
40
|
+
handlerConfig: Partial<HandlerConfig> | null;
|
|
41
|
+
issues: string[];
|
|
42
|
+
suggestions: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare function analyzeSchema(projectPath: string): SchemaAnalysis;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=schema-analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-analyze.d.ts","sourceRoot":"","sources":["../../src/tools/schema-analyze.ts"],"names":[],"mappings":"AAGA,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,aAAa;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AA2ND,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAkJjE"}
|