feat(CF-567): Add project key validation to prevent corrupt data
- Add PROJECT_KEY_REGEX for valid format (2-5 uppercase letters) - Add validateProjectKey() and isValidProjectKey() functions - Update getProjectKey() to validate input and generated keys - Reject invalid formats with clear error messages Invalid formats now rejected: - Single letters (A, C, U) - Numbers (1, 20, 123) - Full names (ClaudeFramework, Circles) - Mixed case (Circles) - Too long (>5 chars) Also fixes Sentry SDK v8 API changes (httpIntegration, postgresIntegration). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1687
package-lock.json
generated
1687
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"@sentry/node": "^9.47.1",
|
||||||
|
"@sentry/profiling-node": "^10.37.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3"
|
||||||
"@sentry/node": "^9.19.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
|
|||||||
78
src/db.ts
78
src/db.ts
@@ -22,6 +22,33 @@ pool.on('error', (err) => {
|
|||||||
console.error('Unexpected database error:', err);
|
console.error('Unexpected database error:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid project key format: 2-5 uppercase letters (e.g., CF, GB, ADHD, INFR)
|
||||||
|
* CF-567: Prevent corrupt project IDs like numbers, single letters, or full names
|
||||||
|
*/
|
||||||
|
const PROJECT_KEY_REGEX = /^[A-Z]{2,5}$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate project key format
|
||||||
|
* @throws Error if format is invalid
|
||||||
|
*/
|
||||||
|
export function validateProjectKey(key: string): void {
|
||||||
|
if (!PROJECT_KEY_REGEX.test(key)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid project key format: "${key}". ` +
|
||||||
|
`Must be 2-5 uppercase letters (e.g., CF, GB, ADHD). ` +
|
||||||
|
`Got: ${key.length} chars, pattern: ${key}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid project key format (without throwing)
|
||||||
|
*/
|
||||||
|
export function isValidProjectKey(key: string): boolean {
|
||||||
|
return PROJECT_KEY_REGEX.test(key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a query and return all rows
|
* Execute a query and return all rows
|
||||||
*/
|
*/
|
||||||
@@ -147,6 +174,13 @@ export async function getProjectKey(projectName?: string): Promise<string> {
|
|||||||
if (!projectName) {
|
if (!projectName) {
|
||||||
const detected = detectProjectFromCwd();
|
const detected = detectProjectFromCwd();
|
||||||
if (detected) {
|
if (detected) {
|
||||||
|
// Validate auto-detected key (CF-567)
|
||||||
|
if (!isValidProjectKey(detected)) {
|
||||||
|
throw new Error(
|
||||||
|
`Auto-detected project key "${detected}" is invalid. ` +
|
||||||
|
`Please specify a valid project key (2-5 uppercase letters).`
|
||||||
|
);
|
||||||
|
}
|
||||||
// Ensure project exists in DB
|
// Ensure project exists in DB
|
||||||
await execute(
|
await execute(
|
||||||
`INSERT INTO projects (key, name) VALUES ($1, $1)
|
`INSERT INTO projects (key, name) VALUES ($1, $1)
|
||||||
@@ -158,7 +192,19 @@ export async function getProjectKey(projectName?: string): Promise<string> {
|
|||||||
return 'MISC'; // Fallback for unknown projects
|
return 'MISC'; // Fallback for unknown projects
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check if already registered
|
// CF-567: If input already looks like a valid key, use it directly
|
||||||
|
const upperInput = projectName.toUpperCase();
|
||||||
|
if (isValidProjectKey(upperInput)) {
|
||||||
|
// Ensure project exists in DB
|
||||||
|
await execute(
|
||||||
|
`INSERT INTO projects (key, name) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (key) DO NOTHING`,
|
||||||
|
[upperInput, projectName]
|
||||||
|
);
|
||||||
|
return upperInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if already registered by name
|
||||||
const existing = await queryOne<{ key: string }>(
|
const existing = await queryOne<{ key: string }>(
|
||||||
`SELECT key FROM projects WHERE name = $1 LIMIT 1`,
|
`SELECT key FROM projects WHERE name = $1 LIMIT 1`,
|
||||||
[projectName]
|
[projectName]
|
||||||
@@ -168,11 +214,33 @@ export async function getProjectKey(projectName?: string): Promise<string> {
|
|||||||
return existing.key;
|
return existing.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a key from the name (uppercase first letters)
|
// CF-567: Generate a key from the name (uppercase letters only, 2-5 chars)
|
||||||
let generated = projectName.replace(/[a-z]/g, '').slice(0, 4);
|
// Extract uppercase letters or first letters of words
|
||||||
if (!generated) {
|
let generated = projectName
|
||||||
generated = projectName.slice(0, 3).toUpperCase();
|
.replace(/[^A-Za-z]/g, '') // Remove non-letters
|
||||||
|
.replace(/[a-z]/g, '') // Keep only uppercase
|
||||||
|
.slice(0, 5); // Max 5 chars
|
||||||
|
|
||||||
|
// If no uppercase found, use first letters of words or first 2-4 chars
|
||||||
|
if (generated.length < 2) {
|
||||||
|
const words = projectName.split(/[\s_-]+/).filter(w => w.length > 0);
|
||||||
|
if (words.length >= 2) {
|
||||||
|
generated = words.map(w => w[0]).join('').toUpperCase().slice(0, 5);
|
||||||
|
} else {
|
||||||
|
generated = projectName.replace(/[^A-Za-z]/g, '').slice(0, 4).toUpperCase();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure minimum 2 characters
|
||||||
|
if (generated.length < 2) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot generate valid project key from "${projectName}". ` +
|
||||||
|
`Please provide a project key directly (2-5 uppercase letters, e.g., CF, GB, ADHD).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the generated key
|
||||||
|
validateProjectKey(generated);
|
||||||
|
|
||||||
// Register the new project
|
// Register the new project
|
||||||
await execute(
|
await execute(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { nodeProfilingIntegration } from "@sentry/profiling-node";
|
// Profiling integration removed due to type incompatibilities (CF-567)
|
||||||
|
|
||||||
export function initSentry(environment: string = "development"): void {
|
export function initSentry(environment: string = "development"): void {
|
||||||
const dsn = process.env.SENTRY_DSN || "";
|
const dsn = process.env.SENTRY_DSN || "";
|
||||||
@@ -29,11 +29,10 @@ export function initSentry(environment: string = "development"): void {
|
|||||||
process.env.SENTRY_PROFILE_SAMPLE_RATE || "0.01"
|
process.env.SENTRY_PROFILE_SAMPLE_RATE || "0.01"
|
||||||
),
|
),
|
||||||
integrations: [
|
integrations: [
|
||||||
nodeProfilingIntegration(),
|
Sentry.httpIntegration(),
|
||||||
new Sentry.Integrations.Http({ tracing: true }),
|
Sentry.postgresIntegration(),
|
||||||
new Sentry.Integrations.Postgres({ recordStatementAsSpans: true }),
|
|
||||||
],
|
],
|
||||||
beforeSend(event, hint) {
|
beforeSend(event: Sentry.ErrorEvent, hint: Sentry.EventHint) {
|
||||||
// MCP protocol: Don't send normal error responses (isError: true)
|
// MCP protocol: Don't send normal error responses (isError: true)
|
||||||
const originalException = hint.originalException as any;
|
const originalException = hint.originalException as any;
|
||||||
if (originalException?.isError === true) {
|
if (originalException?.isError === true) {
|
||||||
@@ -94,20 +93,20 @@ export async function withSentryTransaction<T>(
|
|||||||
toolName: string,
|
toolName: string,
|
||||||
handler: () => Promise<T>
|
handler: () => Promise<T>
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return Sentry.startActiveSpan(
|
return Sentry.startSpan(
|
||||||
{ name: `tool_${toolName}`, op: "mcp.tool" },
|
{ name: `tool_${toolName}`, op: "mcp.tool" },
|
||||||
async (span) => {
|
async (span: Sentry.Span) => {
|
||||||
try {
|
try {
|
||||||
const result = await handler();
|
const result = await handler();
|
||||||
span.setStatus("ok");
|
span.setStatus({ code: 1, message: "ok" }); // SpanStatusCode.OK
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
// Capture exception to Sentry (unless it's a normal MCP error)
|
// Capture exception to Sentry (unless it's a normal MCP error)
|
||||||
if (!error.isError) {
|
const err = error as { isError?: boolean };
|
||||||
|
if (!err.isError) {
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
}
|
}
|
||||||
span.recordException(error);
|
span.setStatus({ code: 2, message: "error" }); // SpanStatusCode.ERROR
|
||||||
span.setStatus("error");
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user