Skip to content

Commit a8f8f6c

Browse files
committed
fix(mcp,cli): close release-blocking auth and publish gaps
1 parent b2e1223 commit a8f8f6c

14 files changed

Lines changed: 920 additions & 438 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### 🔒 Security
6+
- **MCP Streamable Auth**: Enforced strict API key validation in streamable transport (presence-only bypass removed) with constant-time comparison.
7+
- **OAuth/PKCE Hardening**: Fixed PKCE verification flow and required verifier validation for OAuth code exchange.
8+
- **OAuth Client Auth**: Enforced configured `client_id`/`client_secret` validation for token exchange and aligned discovery metadata with supported token auth mode.
9+
10+
### 🛡️ Admin Protection
11+
- **DB Mutation Routes**: Locked down destructive migration/reset/import endpoints behind admin authentication.
12+
13+
### ✅ Release Readiness
14+
- **Regression Coverage**: Added auth and admin-route regression tests for transport auth, PKCE/client auth, and DB mutation authorization.
15+
- **EP CLI Publishability**: Enabled npm release path by removing package `private` flag from `@effect-patterns/ep-cli`.
16+
317
## [0.10.0-patterns] - 2026-01-12
418

519
### 🚀 Features

packages/ep-cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@effect-patterns/ep-cli",
33
"version": "0.2.0",
44
"description": "End-user CLI for Effect Patterns Hub",
5-
"private": true,
5+
"private": false,
66
"type": "module",
77
"bin": {
88
"ep": "./dist/index.js"
@@ -39,4 +39,4 @@
3939
"tsx": "^4.21.0",
4040
"typescript": "5.9.3"
4141
}
42-
}
42+
}

packages/mcp-server/app/api/bulk-import/route.ts

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,43 @@
44

55
import { createDatabase } from "@effect-patterns/toolkit";
66
import { sql } from "drizzle-orm";
7-
import { NextResponse } from "next/server";
7+
import { Effect } from "effect";
8+
import { type NextRequest } from "next/server";
9+
import { ValidationError } from "../../../src/errors";
10+
import { createSimpleHandler } from "../../../src/server/routeHandler";
811

9-
export async function POST(request: Request) {
10-
try {
11-
const body = await request.json() as Record<string, unknown>;
12-
const { patterns } = body;
13-
14-
if (!Array.isArray(patterns)) {
15-
return NextResponse.json(
16-
{
17-
success: false,
18-
error: "patterns must be an array",
19-
},
20-
{ status: 400 },
21-
);
22-
}
23-
24-
const dbUrl = process.env.DATABASE_URL;
25-
if (!dbUrl) {
26-
return NextResponse.json(
27-
{
28-
success: false,
29-
error: "DATABASE_URL not set",
30-
},
31-
{ status: 500 },
32-
);
33-
}
12+
const handleBulkImport = (request: NextRequest) => Effect.gen(function* () {
13+
const body = (yield* Effect.tryPromise({
14+
try: () => request.json(),
15+
catch: () =>
16+
new ValidationError({
17+
field: "body",
18+
message: "Invalid JSON request body",
19+
}),
20+
})) as Record<string, unknown>;
21+
const { patterns } = body;
3422

35-
const { db, close } = createDatabase(dbUrl);
23+
if (!Array.isArray(patterns)) {
24+
return yield* Effect.fail(
25+
new ValidationError({
26+
field: "patterns",
27+
message: "patterns must be an array",
28+
value: patterns,
29+
})
30+
);
31+
}
3632

37-
try {
38-
console.log(`Importing ${patterns.length} patterns...`);
33+
const dbUrl = process.env.DATABASE_URL;
34+
if (!dbUrl) {
35+
return yield* Effect.fail(new Error("DATABASE_URL not set"));
36+
}
3937

40-
let imported = 0;
41-
for (const pattern of patterns) {
42-
await db.execute(sql`
38+
const imported = yield* Effect.tryPromise(async () => {
39+
const { db, close } = createDatabase(dbUrl);
40+
try {
41+
let importedCount = 0;
42+
for (const pattern of patterns as Array<Record<string, unknown>>) {
43+
await db.execute(sql`
4344
INSERT INTO effect_patterns (
4445
id, slug, title, summary, skill_level, category, difficulty,
4546
tags, examples, use_cases, rule, content, author, lesson_order,
@@ -57,28 +58,22 @@ export async function POST(request: Request) {
5758
)
5859
ON CONFLICT (id) DO NOTHING
5960
`);
60-
imported++;
61-
}
61+
importedCount++;
62+
}
63+
return importedCount;
64+
} finally {
65+
await close();
66+
}
67+
});
6268

63-
console.log(`✅ Successfully imported ${imported} patterns`);
69+
return {
70+
success: true,
71+
message: `Imported ${imported} patterns`,
72+
imported,
73+
};
74+
});
6475

65-
return NextResponse.json({
66-
success: true,
67-
message: `Imported ${imported} patterns`,
68-
imported,
69-
});
70-
} finally {
71-
await close();
72-
}
73-
} catch (error) {
74-
console.error("Import error:", error);
75-
return NextResponse.json(
76-
{
77-
success: false,
78-
error: "Import failed",
79-
details: error instanceof Error ? error.message : String(error),
80-
},
81-
{ status: 500 },
82-
);
83-
}
84-
}
76+
export const POST = createSimpleHandler(handleBulkImport, {
77+
requireAuth: false,
78+
requireAdmin: true,
79+
});

packages/mcp-server/app/api/final-reset/route.ts

Lines changed: 63 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,79 +4,73 @@
44

55
import { createDatabase } from "@effect-patterns/toolkit"
66
import { sql } from "drizzle-orm"
7-
import { NextResponse } from "next/server"
7+
import { Effect } from "effect"
8+
import { type NextRequest } from "next/server"
9+
import { createSimpleHandler } from "../../../src/server/routeHandler"
810

9-
export async function POST() {
10-
try {
11-
const dbUrl = process.env.DATABASE_URL
12-
if (!dbUrl) {
13-
return NextResponse.json({
14-
success: false,
15-
error: "DATABASE_URL not set"
16-
}, { status: 500 })
17-
}
11+
const handleFinalReset = (_request: NextRequest) => Effect.gen(function* () {
12+
const dbUrl = process.env.DATABASE_URL
13+
if (!dbUrl) {
14+
return yield* Effect.fail(new Error("DATABASE_URL not set"))
15+
}
1816

17+
yield* Effect.tryPromise(async () => {
1918
const { db, close } = createDatabase(dbUrl)
19+
try {
20+
// Drop existing tables
21+
await db.execute(sql`DROP TABLE IF EXISTS effect_patterns CASCADE`)
22+
await db.execute(sql`DROP TABLE IF EXISTS application_patterns CASCADE`)
2023

21-
// Drop existing tables
22-
await db.execute(sql`DROP TABLE IF EXISTS effect_patterns CASCADE`)
23-
await db.execute(sql`DROP TABLE IF EXISTS application_patterns CASCADE`)
24-
25-
// Create effect_patterns table with individual statements
26-
await db.execute(sql`CREATE TABLE effect_patterns (
27-
id UUID PRIMARY KEY,
28-
slug VARCHAR(255) NOT NULL UNIQUE,
29-
title VARCHAR(500) NOT NULL,
30-
summary TEXT NOT NULL,
31-
skill_level VARCHAR(50) NOT NULL,
32-
category VARCHAR(100),
33-
difficulty VARCHAR(50),
34-
tags JSONB DEFAULT '[]',
35-
examples JSONB DEFAULT '[]',
36-
use_cases JSONB DEFAULT '[]',
37-
rule JSONB,
38-
content TEXT,
39-
author VARCHAR(255),
40-
lesson_order INTEGER,
41-
application_pattern_id UUID REFERENCES application_patterns(id) ON DELETE SET NULL,
42-
validated BOOLEAN DEFAULT false NOT NULL,
43-
validated_at TIMESTAMP,
44-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
45-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
46-
)`)
24+
// Create effect_patterns table with individual statements
25+
await db.execute(sql`CREATE TABLE effect_patterns (
26+
id UUID PRIMARY KEY,
27+
slug VARCHAR(255) NOT NULL UNIQUE,
28+
title VARCHAR(500) NOT NULL,
29+
summary TEXT NOT NULL,
30+
skill_level VARCHAR(50) NOT NULL,
31+
category VARCHAR(100),
32+
difficulty VARCHAR(50),
33+
tags JSONB DEFAULT '[]',
34+
examples JSONB DEFAULT '[]',
35+
use_cases JSONB DEFAULT '[]',
36+
rule JSONB,
37+
content TEXT,
38+
author VARCHAR(255),
39+
lesson_order INTEGER,
40+
application_pattern_id UUID REFERENCES application_patterns(id) ON DELETE SET NULL,
41+
validated BOOLEAN DEFAULT false NOT NULL,
42+
validated_at TIMESTAMP,
43+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
44+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
45+
)`)
4746

48-
// Create application_patterns table with individual statements
49-
await db.execute(sql`CREATE TABLE application_patterns (
50-
id UUID PRIMARY KEY,
51-
slug VARCHAR(255) NOT NULL UNIQUE,
52-
name VARCHAR(255) NOT NULL,
53-
description TEXT NOT NULL,
54-
learning_order INTEGER NOT NULL,
55-
effect_module VARCHAR(100),
56-
sub_patterns JSONB DEFAULT '[]',
57-
validated BOOLEAN DEFAULT false NOT NULL,
58-
validated_at TIMESTAMP,
59-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
60-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
61-
)`)
62-
63-
await close()
47+
// Create application_patterns table with individual statements
48+
await db.execute(sql`CREATE TABLE application_patterns (
49+
id UUID PRIMARY KEY,
50+
slug VARCHAR(255) NOT NULL UNIQUE,
51+
name VARCHAR(255) NOT NULL,
52+
description TEXT NOT NULL,
53+
learning_order INTEGER NOT NULL,
54+
effect_module VARCHAR(100),
55+
sub_patterns JSONB DEFAULT '[]',
56+
validated BOOLEAN DEFAULT false NOT NULL,
57+
validated_at TIMESTAMP,
58+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
59+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
60+
)`)
61+
} finally {
62+
await close()
63+
}
64+
})
6465

65-
return NextResponse.json({
66-
success: true,
67-
message: "Database reset completed with full schema",
68-
tablesRecreated: 2
69-
})
70-
} catch (error) {
71-
console.error("Reset error:", error)
72-
const errorMessage = error instanceof Error ? error.message : String(error)
73-
return NextResponse.json(
74-
{
75-
success: false,
76-
error: "Database reset failed",
77-
details: errorMessage
78-
},
79-
{ status: 500 }
80-
)
66+
return {
67+
success: true,
68+
message: "Database reset completed with full schema",
69+
tablesRecreated: 2
8170
}
82-
}
71+
})
72+
73+
export const POST = createSimpleHandler(handleFinalReset, {
74+
requireAuth: false,
75+
requireAdmin: true,
76+
})

0 commit comments

Comments
 (0)