@digestseo/mcp-gsc 0.4.21 → 0.4.23
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/CHANGELOG.md +13 -0
- package/README.md +3 -1
- package/package.json +3 -3
- package/src/index.ts +272 -17
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.23] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- The OAuth proxy now requires an explicit, CSRF-bound consent step that identifies the requesting MCP client and requested permissions before forwarding the browser to Google, preventing cached upstream Google consent from silently authorizing an untrusted MCP client. User or Google denial is returned to the validated MCP client as a terminal OAuth error instead of stranding the connection flow on a local error page.
|
|
14
|
+
|
|
15
|
+
## [0.4.22] - 2026-09-22
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- The repository's Cursor plugin now bundles native `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities` Agent Skills that compose existing MCP tools into evidence-safe Search Console workflows without adding a second tool surface.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- The production Wrangler floor is now 4.136.3, and the Cloudflare Vitest plugin floor is 1.2.3, so runtime/test installs use Cloudflare's patched Wrangler line for the bundled `smol-toml` denial-of-service advisories GHSA-7w5x-hrqm-74c2 and GHSA-v3rj-xjv7-4jmq.
|
|
22
|
+
|
|
10
23
|
## [0.4.21] - 2026-09-22
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ claude mcp add --transport http gsc http://127.0.0.1:8080/mcp
|
|
|
56
56
|
|
|
57
57
|
[](https://cursor.com/en/install-mcp?name=gsc&config=eyJ1cmwiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvbWNwIn0%3D)
|
|
58
58
|
|
|
59
|
+
When this repository is installed as a Cursor plugin, Cursor also auto-discovers three bundled Agent Skills that compose the MCP tools into evidence-safe workflows: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. The **Add to Cursor** button above installs the MCP connection only; the bundled skills are part of the repository's Cursor plugin package.
|
|
60
|
+
|
|
59
61
|
**Kiro**
|
|
60
62
|
|
|
61
63
|
[](https://kiro.dev/launch/mcp/add?name=mcp-gsc&config=%7B%22url%22%3A%22http%3A%2F%2F127.0.0.1%3A8080%2Fmcp%22%2C%22disabled%22%3Afalse%2C%22autoApprove%22%3A%5B%5D%7D)
|
|
@@ -66,7 +68,7 @@ Start the npm launcher before using either install button. Complete its Google O
|
|
|
66
68
|
|
|
67
69
|
A self-hostable [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **Google Search Console**. Connect it to Claude.ai, Cursor, ChatGPT, or any MCP-compatible client and ask your AI assistant about your site's organic search performance — impressions, clicks, top queries, index status, and sitemap health — straight from your own Google account.
|
|
68
70
|
|
|
69
|
-
It runs on [Cloudflare Workers](https://workers.cloudflare.com/) and ships with
|
|
71
|
+
It runs on [Cloudflare Workers](https://workers.cloudflare.com/) and ships with Google OAuth onboarding: connect the server in your client, review the mcp-gsc consent page that identifies the requesting MCP client and permissions, then sign in with Google and grant the requested Google Search Console permissions. No API keys to copy around and no service-account JSON to manage.
|
|
70
72
|
|
|
71
73
|
## Tools
|
|
72
74
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digestseo/mcp-gsc",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "scripts/glama-start.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -76,11 +76,11 @@
|
|
|
76
76
|
"@cloudflare/workers-oauth-provider": "^0.10.3",
|
|
77
77
|
"@modelcontextprotocol/server": "2.0.0",
|
|
78
78
|
"agents": "^0.24.0",
|
|
79
|
-
"wrangler": "^4.
|
|
79
|
+
"wrangler": "^4.136.3",
|
|
80
80
|
"zod": "^4.4.3"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@cloudflare/vitest-plugin": "^1.
|
|
83
|
+
"@cloudflare/vitest-plugin": "^1.2.3",
|
|
84
84
|
"@cloudflare/workers-types": "^5.20260919.1",
|
|
85
85
|
"tsx": "^4.20.0",
|
|
86
86
|
"typescript": "^6.0.3",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
1
|
+
import { OAuthProvider, type AuthRequest } from '@cloudflare/workers-oauth-provider';
|
|
2
2
|
import { DurableObject } from 'cloudflare:workers';
|
|
3
3
|
import { createMcpHandler, getMcpAuthContext } from 'agents/mcp/server';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
import { WRITE_TOOL_ANNOTATIONS } from './write-tool-annotations';
|
|
72
72
|
import {
|
|
73
73
|
getToolCatalogForAccessMode,
|
|
74
|
+
getGoogleOAuthScopes,
|
|
74
75
|
resolveGscAccessMode,
|
|
75
76
|
type GscAccessMode,
|
|
76
77
|
} from './access-mode';
|
|
@@ -2134,6 +2135,154 @@ export class PendingAuthState extends DurableObject<Env> {
|
|
|
2134
2135
|
}
|
|
2135
2136
|
}
|
|
2136
2137
|
|
|
2138
|
+
const CONSENT_COOKIE_HTTPS = '__Host-MCP_GSC_CONSENT';
|
|
2139
|
+
const CONSENT_COOKIE_LOOPBACK = 'MCP_GSC_CONSENT';
|
|
2140
|
+
|
|
2141
|
+
interface PendingConsentPayload {
|
|
2142
|
+
kind: 'mcp-gsc-consent';
|
|
2143
|
+
authRequest: AuthRequest;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function isLoopbackHttp(url: URL): boolean {
|
|
2147
|
+
return (
|
|
2148
|
+
url.protocol === 'http:' &&
|
|
2149
|
+
(url.hostname === '127.0.0.1' ||
|
|
2150
|
+
url.hostname === 'localhost' ||
|
|
2151
|
+
url.hostname === '[::1]')
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function consentCookieName(url: URL): string {
|
|
2156
|
+
return isLoopbackHttp(url) ? CONSENT_COOKIE_LOOPBACK : CONSENT_COOKIE_HTTPS;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function consentCookie(url: URL, value: string, maxAgeSeconds = 600): string {
|
|
2160
|
+
const secure = isLoopbackHttp(url) ? '' : '; Secure';
|
|
2161
|
+
return `${consentCookieName(url)}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAgeSeconds}${secure}`;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function clearConsentCookie(url: URL): string {
|
|
2165
|
+
return consentCookie(url, '', 0);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function getCookie(request: Request, name: string): string | null {
|
|
2169
|
+
const cookie = request.headers.get('cookie');
|
|
2170
|
+
if (!cookie) return null;
|
|
2171
|
+
for (const entry of cookie.split(';')) {
|
|
2172
|
+
const separator = entry.indexOf('=');
|
|
2173
|
+
if (separator < 0) continue;
|
|
2174
|
+
if (entry.slice(0, separator).trim() !== name) continue;
|
|
2175
|
+
try {
|
|
2176
|
+
return decodeURIComponent(entry.slice(separator + 1).trim());
|
|
2177
|
+
} catch {
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
async function constantTimeStringEqual(left: string, right: string): Promise<boolean> {
|
|
2185
|
+
const encoder = new TextEncoder();
|
|
2186
|
+
const [leftHash, rightHash] = await Promise.all([
|
|
2187
|
+
crypto.subtle.digest('SHA-256', encoder.encode(left)),
|
|
2188
|
+
crypto.subtle.digest('SHA-256', encoder.encode(right)),
|
|
2189
|
+
]);
|
|
2190
|
+
const leftBytes = new Uint8Array(leftHash);
|
|
2191
|
+
const rightBytes = new Uint8Array(rightHash);
|
|
2192
|
+
let difference = 0;
|
|
2193
|
+
for (let index = 0; index < leftBytes.length; index += 1) {
|
|
2194
|
+
difference |= leftBytes[index] ^ rightBytes[index];
|
|
2195
|
+
}
|
|
2196
|
+
return difference === 0;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function escapeHtml(value: string): string {
|
|
2200
|
+
return value
|
|
2201
|
+
.replace(/&/g, '&')
|
|
2202
|
+
.replace(/</g, '<')
|
|
2203
|
+
.replace(/>/g, '>')
|
|
2204
|
+
.replace(/"/g, '"')
|
|
2205
|
+
.replace(/'/g, ''');
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function isParsedAuthorizationRequest(value: unknown): value is AuthRequest {
|
|
2209
|
+
if (!value || typeof value !== 'object') return false;
|
|
2210
|
+
const candidate = value as Partial<AuthRequest>;
|
|
2211
|
+
return (
|
|
2212
|
+
typeof candidate.responseType === 'string' &&
|
|
2213
|
+
typeof candidate.clientId === 'string' &&
|
|
2214
|
+
typeof candidate.redirectUri === 'string' &&
|
|
2215
|
+
Array.isArray(candidate.scope) &&
|
|
2216
|
+
candidate.scope.every((scope) => typeof scope === 'string') &&
|
|
2217
|
+
typeof candidate.state === 'string'
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function isPendingConsentPayload(value: unknown): value is PendingConsentPayload {
|
|
2222
|
+
if (!value || typeof value !== 'object') return false;
|
|
2223
|
+
const candidate = value as Partial<PendingConsentPayload>;
|
|
2224
|
+
return candidate.kind === 'mcp-gsc-consent' && isParsedAuthorizationRequest(candidate.authRequest);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function oauthErrorRedirect(
|
|
2228
|
+
authRequest: AuthRequest,
|
|
2229
|
+
code: 'access_denied' | 'server_error',
|
|
2230
|
+
description?: string,
|
|
2231
|
+
): Response {
|
|
2232
|
+
const redirect = new URL(authRequest.redirectUri);
|
|
2233
|
+
redirect.searchParams.set('error', code);
|
|
2234
|
+
if (description) redirect.searchParams.set('error_description', description);
|
|
2235
|
+
redirect.searchParams.set('state', authRequest.state);
|
|
2236
|
+
if (authRequest.issuer) redirect.searchParams.set('iss', authRequest.issuer);
|
|
2237
|
+
return new Response(null, {
|
|
2238
|
+
status: 302,
|
|
2239
|
+
headers: { location: redirect.toString() },
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function consentHtml(input: {
|
|
2244
|
+
clientName: string;
|
|
2245
|
+
clientId: string;
|
|
2246
|
+
consentNonce: string;
|
|
2247
|
+
mcpScopes: readonly string[];
|
|
2248
|
+
googleScopes: readonly string[];
|
|
2249
|
+
accessMode: GscAccessMode;
|
|
2250
|
+
}): string {
|
|
2251
|
+
const list = (values: readonly string[]) =>
|
|
2252
|
+
values.length > 0
|
|
2253
|
+
? values.map((value) => `<li><code>${escapeHtml(value)}</code></li>`).join('')
|
|
2254
|
+
: '<li><em>No additional scopes requested</em></li>';
|
|
2255
|
+
return `<!doctype html>
|
|
2256
|
+
<html lang="en">
|
|
2257
|
+
<head>
|
|
2258
|
+
<meta charset="utf-8">
|
|
2259
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2260
|
+
<title>Authorize Google Search Console MCP</title>
|
|
2261
|
+
<style>
|
|
2262
|
+
body{font-family:system-ui,sans-serif;max-width:760px;margin:3rem auto;padding:0 1rem;line-height:1.5;color:#111}main{border:1px solid #ddd;border-radius:12px;padding:1.5rem}code{overflow-wrap:anywhere}button{padding:.65rem 1rem;margin-right:.5rem;font:inherit}button[name="decision"][value="allow"]{font-weight:700}.muted{color:#555}
|
|
2263
|
+
</style>
|
|
2264
|
+
</head>
|
|
2265
|
+
<body>
|
|
2266
|
+
<main>
|
|
2267
|
+
<h1>Authorize Google Search Console access</h1>
|
|
2268
|
+
<p><strong>${escapeHtml(input.clientName)}</strong> is requesting access through mcp-gsc.</p>
|
|
2269
|
+
<p class="muted">Client ID: <code>${escapeHtml(input.clientId)}</code></p>
|
|
2270
|
+
<h2>MCP scopes requested</h2>
|
|
2271
|
+
<ul>${list(input.mcpScopes)}</ul>
|
|
2272
|
+
<h2>Google scopes mcp-gsc will request</h2>
|
|
2273
|
+
<p>Configured access mode: <strong>${escapeHtml(input.accessMode)}</strong></p>
|
|
2274
|
+
<ul>${list(input.googleScopes)}</ul>
|
|
2275
|
+
<p>Continue only if you recognize and trust the requesting MCP client.</p>
|
|
2276
|
+
<form method="post" action="/authorize">
|
|
2277
|
+
<input type="hidden" name="consent_nonce" value="${escapeHtml(input.consentNonce)}">
|
|
2278
|
+
<button type="submit" name="decision" value="allow">Allow and continue to Google</button>
|
|
2279
|
+
<button type="submit" name="decision" value="deny">Deny</button>
|
|
2280
|
+
</form>
|
|
2281
|
+
</main>
|
|
2282
|
+
</body>
|
|
2283
|
+
</html>`;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2137
2286
|
function googleRedirectUri(request: Request): string {
|
|
2138
2287
|
return new URL('/google/callback', request.url).toString();
|
|
2139
2288
|
}
|
|
@@ -2169,7 +2318,7 @@ export const defaultHandler = {
|
|
|
2169
2318
|
});
|
|
2170
2319
|
}
|
|
2171
2320
|
|
|
2172
|
-
if (url.pathname === '/authorize') {
|
|
2321
|
+
if (url.pathname === '/authorize' && request.method === 'GET') {
|
|
2173
2322
|
let claudeAuthRequest;
|
|
2174
2323
|
try {
|
|
2175
2324
|
claudeAuthRequest = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
@@ -2185,6 +2334,9 @@ export const defaultHandler = {
|
|
|
2185
2334
|
},
|
|
2186
2335
|
);
|
|
2187
2336
|
}
|
|
2337
|
+
if (!isParsedAuthorizationRequest(claudeAuthRequest)) {
|
|
2338
|
+
return new Response('Invalid OAuth authorization request.', { status: 400 });
|
|
2339
|
+
}
|
|
2188
2340
|
let accessMode: GscAccessMode;
|
|
2189
2341
|
try {
|
|
2190
2342
|
accessMode = resolveGscAccessMode(env.GSC_ACCESS_MODE);
|
|
@@ -2194,16 +2346,106 @@ export const defaultHandler = {
|
|
|
2194
2346
|
{ status: 500 },
|
|
2195
2347
|
);
|
|
2196
2348
|
}
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2349
|
+
let client;
|
|
2350
|
+
try {
|
|
2351
|
+
client = await env.OAUTH_PROVIDER.lookupClient(claudeAuthRequest.clientId);
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
console.warn('OAuth client lookup failed', {
|
|
2354
|
+
message: (err as Error).message,
|
|
2355
|
+
});
|
|
2356
|
+
return new Response('Could not verify the requesting OAuth client.', { status: 400 });
|
|
2357
|
+
}
|
|
2358
|
+
if (!client) {
|
|
2359
|
+
return new Response('Unknown OAuth client.', { status: 400 });
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const consentNonce = crypto.randomUUID();
|
|
2363
|
+
const pendingConsent: PendingConsentPayload = {
|
|
2364
|
+
kind: 'mcp-gsc-consent',
|
|
2365
|
+
authRequest: claudeAuthRequest,
|
|
2366
|
+
};
|
|
2367
|
+
await stashPendingAuth(env, consentNonce, pendingConsent);
|
|
2368
|
+
return new Response(
|
|
2369
|
+
consentHtml({
|
|
2370
|
+
clientName: client.clientName ?? claudeAuthRequest.clientId,
|
|
2371
|
+
clientId: claudeAuthRequest.clientId,
|
|
2372
|
+
consentNonce,
|
|
2373
|
+
mcpScopes: claudeAuthRequest.scope,
|
|
2374
|
+
googleScopes: getGoogleOAuthScopes(accessMode),
|
|
2375
|
+
accessMode,
|
|
2376
|
+
}),
|
|
2377
|
+
{
|
|
2378
|
+
headers: {
|
|
2379
|
+
'content-type': 'text/html; charset=utf-8',
|
|
2380
|
+
'cache-control': 'no-store',
|
|
2381
|
+
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
2382
|
+
'referrer-policy': 'no-referrer',
|
|
2383
|
+
'x-content-type-options': 'nosniff',
|
|
2384
|
+
'x-frame-options': 'DENY',
|
|
2385
|
+
'set-cookie': consentCookie(url, consentNonce),
|
|
2386
|
+
},
|
|
2387
|
+
},
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
if (url.pathname === '/authorize' && request.method === 'POST') {
|
|
2392
|
+
const contentType = request.headers.get('content-type')?.toLowerCase() ?? '';
|
|
2393
|
+
if (!contentType.startsWith('application/x-www-form-urlencoded')) {
|
|
2394
|
+
return new Response('Unsupported Media Type', { status: 415 });
|
|
2395
|
+
}
|
|
2396
|
+
const contentLength = Number(request.headers.get('content-length') ?? '0');
|
|
2397
|
+
if (Number.isFinite(contentLength) && contentLength > 4096) {
|
|
2398
|
+
return new Response('Authorization response too large', { status: 413 });
|
|
2399
|
+
}
|
|
2400
|
+
const body = await request.formData();
|
|
2401
|
+
const consentNonceValue = body.get('consent_nonce');
|
|
2402
|
+
const decisionValue = body.get('decision');
|
|
2403
|
+
const consentNonce =
|
|
2404
|
+
typeof consentNonceValue === 'string' ? consentNonceValue : null;
|
|
2405
|
+
const decision = typeof decisionValue === 'string' ? decisionValue : null;
|
|
2406
|
+
const cookieNonce = getCookie(request, consentCookieName(url));
|
|
2407
|
+
if (!consentNonce || !cookieNonce || !(await constantTimeStringEqual(consentNonce, cookieNonce))) {
|
|
2408
|
+
return new Response('Invalid or expired authorization consent.', { status: 400 });
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
const pending = await consumePendingAuth(env, consentNonce);
|
|
2412
|
+
if (!pending || !isPendingConsentPayload(pending.claudeAuthRequest)) {
|
|
2413
|
+
return new Response('Authorization consent expired or invalid.', { status: 400 });
|
|
2414
|
+
}
|
|
2415
|
+
const authRequest = pending.claudeAuthRequest.authRequest;
|
|
2416
|
+
if (decision === 'deny') {
|
|
2417
|
+
const denied = oauthErrorRedirect(authRequest, 'access_denied', 'The user denied access.');
|
|
2418
|
+
denied.headers.append('set-cookie', clearConsentCookie(url));
|
|
2419
|
+
return denied;
|
|
2420
|
+
}
|
|
2421
|
+
if (decision !== 'allow') {
|
|
2422
|
+
return new Response('Invalid authorization decision.', { status: 400 });
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
let accessMode: GscAccessMode;
|
|
2426
|
+
try {
|
|
2427
|
+
accessMode = resolveGscAccessMode(env.GSC_ACCESS_MODE);
|
|
2428
|
+
} catch {
|
|
2429
|
+
return new Response(
|
|
2430
|
+
'Server configuration error: GSC_ACCESS_MODE must be "readonly" or "readwrite".',
|
|
2431
|
+
{ status: 500 },
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
const googleNonce = crypto.randomUUID();
|
|
2435
|
+
await stashPendingAuth(env, googleNonce, authRequest);
|
|
2200
2436
|
const googleUrl = buildAuthUrl(
|
|
2201
2437
|
env.GOOGLE_CLIENT_ID,
|
|
2202
|
-
|
|
2203
|
-
|
|
2438
|
+
googleRedirectUri(request),
|
|
2439
|
+
googleNonce,
|
|
2204
2440
|
accessMode,
|
|
2205
2441
|
);
|
|
2206
|
-
return Response
|
|
2442
|
+
return new Response(null, {
|
|
2443
|
+
status: 302,
|
|
2444
|
+
headers: {
|
|
2445
|
+
location: googleUrl,
|
|
2446
|
+
'set-cookie': clearConsentCookie(url),
|
|
2447
|
+
},
|
|
2448
|
+
});
|
|
2207
2449
|
}
|
|
2208
2450
|
|
|
2209
2451
|
if (url.pathname === '/google/callback') {
|
|
@@ -2211,14 +2453,7 @@ export const defaultHandler = {
|
|
|
2211
2453
|
const state = url.searchParams.get('state');
|
|
2212
2454
|
const googleError = url.searchParams.get('error');
|
|
2213
2455
|
|
|
2214
|
-
if (googleError) {
|
|
2215
|
-
console.error('Google OAuth returned error', { error: googleError });
|
|
2216
|
-
return new Response(`Google OAuth error: ${googleError}`, {
|
|
2217
|
-
status: 400,
|
|
2218
|
-
});
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
if (!code || !state) {
|
|
2456
|
+
if (!state || (!code && !googleError)) {
|
|
2222
2457
|
console.error('Missing code or state on /google/callback', {
|
|
2223
2458
|
has_code: !!code,
|
|
2224
2459
|
has_state: !!state,
|
|
@@ -2232,6 +2467,27 @@ export const defaultHandler = {
|
|
|
2232
2467
|
return new Response('Auth request expired or invalid', { status: 400 });
|
|
2233
2468
|
}
|
|
2234
2469
|
|
|
2470
|
+
const claudeAuthReq = pending.claudeAuthRequest;
|
|
2471
|
+
if (!isParsedAuthorizationRequest(claudeAuthReq)) {
|
|
2472
|
+
console.error('Pending auth had an invalid shape');
|
|
2473
|
+
return new Response('Auth request expired or invalid', { status: 400 });
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (googleError) {
|
|
2477
|
+
console.warn('Google OAuth returned error', { error: googleError });
|
|
2478
|
+
return oauthErrorRedirect(
|
|
2479
|
+
claudeAuthReq,
|
|
2480
|
+
googleError === 'access_denied' ? 'access_denied' : 'server_error',
|
|
2481
|
+
googleError === 'access_denied'
|
|
2482
|
+
? 'Google authorization was denied.'
|
|
2483
|
+
: 'Google authorization failed.',
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
if (!code) {
|
|
2488
|
+
return new Response('Missing authorization code', { status: 400 });
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2235
2491
|
const redirectUri = googleRedirectUri(request);
|
|
2236
2492
|
|
|
2237
2493
|
let tokens;
|
|
@@ -2269,7 +2525,6 @@ export const defaultHandler = {
|
|
|
2269
2525
|
return new Response('Save user failed', { status: 500 });
|
|
2270
2526
|
}
|
|
2271
2527
|
|
|
2272
|
-
const claudeAuthReq = pending.claudeAuthRequest as any;
|
|
2273
2528
|
let redirectTo: string;
|
|
2274
2529
|
try {
|
|
2275
2530
|
({ redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({
|