Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions web/app/api/auth/espn/credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ export async function GET(request: NextRequest) {
const forEdit = request.nextUrl.searchParams.get('forEdit') === 'true';
const queryString = forEdit ? '?forEdit=true' : '';

const bearer = (await getToken?.()) || undefined;
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/credentials/espn${queryString}`, {
headers: {
...(bearer ? { 'Authorization': `Bearer ${bearer}` } : {})
'Authorization': `Bearer ${bearer}`,
},
});

Expand Down Expand Up @@ -101,12 +104,15 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'NEXT_PUBLIC_AUTH_WORKER_URL is not configured' }, { status: 500 });
}

const bearer = (await getToken?.()) || undefined;
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/credentials/espn`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(bearer ? { 'Authorization': `Bearer ${bearer}` } : {})
'Authorization': `Bearer ${bearer}`,
},
body: JSON.stringify({
swid: body.swid,
Expand Down
12 changes: 9 additions & 3 deletions web/app/api/connect/sleeper/discover/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'AUTH_WORKER_URL not configured' }, { status: 500 });
}

const body = await req.json().catch(() => ({}));
const bearer = (await getToken?.()) || undefined;
const body = await req.json().catch(() => null);
if (!body || typeof body !== 'object') {
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/connect/sleeper/discover`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
Authorization: `Bearer ${bearer}`,
},
body: JSON.stringify(body),
});
Expand Down
9 changes: 6 additions & 3 deletions web/app/api/connect/sleeper/leagues/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ export async function DELETE(
return NextResponse.json({ error: 'AUTH_WORKER_URL not configured' }, { status: 500 });
}

const bearer = (await getToken?.()) || undefined;
const workerRes = await fetch(`${authWorkerUrl}/leagues/sleeper/${leagueId}`, {
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/leagues/sleeper/${encodeURIComponent(leagueId)}`, {
method: 'DELETE',
headers: {
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
Authorization: `Bearer ${bearer}`,
},
});

Expand Down
18 changes: 16 additions & 2 deletions web/app/api/connect/yahoo/authorize/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,31 @@ export async function GET() {
return NextResponse.json({ error: 'AUTH_WORKER_URL not configured' }, { status: 500 });
}

const bearer = (await getToken?.()) || undefined;
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/connect/yahoo/authorize`, {
redirect: 'manual',
headers: {
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
Authorization: `Bearer ${bearer}`,
},
});

// auth-worker returns 302 with Location header
const location = workerRes.headers.get('Location');
if (workerRes.status === 302 && location) {
// Validate redirect target to prevent open redirect attacks
try {
const redirectUrl = new URL(location);
if (redirectUrl.hostname !== 'api.login.yahoo.com' && !redirectUrl.hostname.endsWith('.yahoo.com')) {
console.error('Yahoo authorize: unexpected redirect target:', redirectUrl.hostname);
return NextResponse.json({ error: 'Invalid redirect target' }, { status: 502 });
}
Comment on lines +38 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For improved readability, you could simplify the domain validation logic by first checking for valid domains and then handling the invalid case. This avoids a complex negative condition and makes the intent clearer.

        const isYahooDomain = redirectUrl.hostname === 'api.login.yahoo.com' || redirectUrl.hostname.endsWith('.yahoo.com');
        if (!isYahooDomain) {
          console.error('Yahoo authorize: unexpected redirect target:', redirectUrl.hostname);
          return NextResponse.json({ error: 'Invalid redirect target' }, { status: 502 });
        }

} catch (e) {
console.error('Yahoo authorize: failed to parse redirect URL', e);
return NextResponse.json({ error: 'Invalid redirect URL' }, { status: 502 });
}
return NextResponse.redirect(location);
}

Expand Down
7 changes: 5 additions & 2 deletions web/app/api/connect/yahoo/discover/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ export async function POST() {
return NextResponse.json({ error: 'AUTH_WORKER_URL not configured' }, { status: 500 });
}

const bearer = (await getToken?.()) || undefined;
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/connect/yahoo/discover`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
Authorization: `Bearer ${bearer}`,
},
});

Expand Down
9 changes: 6 additions & 3 deletions web/app/api/connect/yahoo/leagues/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ export async function DELETE(
return NextResponse.json({ error: 'AUTH_WORKER_URL not configured' }, { status: 500 });
}

const bearer = (await getToken?.()) || undefined;
const workerRes = await fetch(`${authWorkerUrl}/leagues/yahoo/${leagueId}`, {
const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({ error: 'Authentication token unavailable' }, { status: 401 });
}
const workerRes = await fetch(`${authWorkerUrl}/leagues/yahoo/${encodeURIComponent(leagueId)}`, {
method: 'DELETE',
headers: {
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
Authorization: `Bearer ${bearer}`,
},
});

Expand Down
39 changes: 32 additions & 7 deletions web/app/api/debug/test-mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@ import { auth } from '@clerk/nextjs/server';
const ALLOWED_MCP_HOST_PATTERNS = [
// Flaim production domains
'flaim.app',
// Cloudflare Workers (any account - for contributors and preview)
'workers.dev',
// Localhost for development
'localhost',
'127.0.0.1',
// Localhost for development (excluded in production)
...(process.env.NODE_ENV === 'development' ? ['localhost', '127.0.0.1'] : []),
];

/**
* Flaim CF account subdomain. Workers are <name>.gerrygugger.workers.dev.
*/
const CF_ACCOUNT_SUBDOMAIN = 'gerrygugger';

/**
* Flaim-specific worker name prefixes allowed on workers.dev.
* Prevents probing arbitrary Cloudflare Workers.
*/
const ALLOWED_WORKER_PREFIXES = [
'fantasy-mcp',
'fantasy-mcp-preview',
'auth-worker',
'auth-worker-preview',
];

/**
Expand All @@ -28,10 +41,22 @@ function isAllowedUrl(urlString: string): boolean {
return false;
}

// Check against allowlist patterns
return ALLOWED_MCP_HOST_PATTERNS.some(pattern =>
// Check against static allowlist patterns
const matchesStatic = ALLOWED_MCP_HOST_PATTERNS.some(pattern =>
url.hostname === pattern || url.hostname.endsWith(`.${pattern}`)
);
if (matchesStatic) return true;

// Check workers.dev — only allow known Flaim worker prefixes on our account
// Format: <worker>.<account>.workers.dev (exactly 4 segments)
if (url.hostname.endsWith('.workers.dev')) {
const parts = url.hostname.split('.');
return parts.length === 4
&& parts[1] === CF_ACCOUNT_SUBDOMAIN
&& ALLOWED_WORKER_PREFIXES.some(prefix => parts[0] === prefix);
}

return false;
} catch {
return false;
}
Expand Down
Loading
Loading