mcp-google-gsc 1.0.15 → 1.1.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/auth-cli.d.ts +2 -0
- package/dist/auth-cli.js +292 -0
- package/dist/auth-cli.js.map +7 -0
- package/dist/build-info.json +5 -1
- package/dist/credentials.d.ts +21 -0
- package/dist/credentials.js +93 -0
- package/dist/credentials.js.map +7 -0
- package/dist/embedded-secrets.d.ts +3 -0
- package/dist/embedded-secrets.js +11 -0
- package/dist/embedded-secrets.js.map +7 -0
- package/dist/errors.js +53 -60
- package/dist/errors.js.map +7 -0
- package/dist/index.js +375 -369
- package/dist/index.js.map +7 -0
- package/dist/platform.d.ts +6 -0
- package/dist/platform.js +51 -0
- package/dist/platform.js.map +7 -0
- package/dist/resilience.js +92 -93
- package/dist/resilience.js.map +7 -0
- package/dist/tools.js +86 -82
- package/dist/tools.js.map +7 -0
- package/package.json +12 -4
package/dist/auth-cli.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { google } from "googleapis";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import promptsImport from "prompts";
|
|
5
|
+
import { URL } from "url";
|
|
6
|
+
import { writeStoredCredentials, credentialsFilePath, CREDENTIALS_FILE_VERSION } from "./credentials.js";
|
|
7
|
+
import { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from "./embedded-secrets.js";
|
|
8
|
+
import { classifyError, GscAuthError } from "./errors.js";
|
|
9
|
+
import { findFreeLoopbackPort, openBrowser } from "./platform.js";
|
|
10
|
+
import { logger, withResilience } from "./resilience.js";
|
|
11
|
+
const prompts = promptsImport.default ?? promptsImport;
|
|
12
|
+
const OAUTH_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
|
|
13
|
+
const OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
14
|
+
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const args = { help: false };
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
20
|
+
else if (a === "--site-url" && argv[i + 1]) {
|
|
21
|
+
args.siteUrl = argv[++i];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
function printHelp() {
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
[
|
|
29
|
+
"mcp-gsc-auth -- authorize Claude to access your Google Search Console data",
|
|
30
|
+
"",
|
|
31
|
+
"Usage:",
|
|
32
|
+
" npx mcp-gsc-auth",
|
|
33
|
+
" npx mcp-gsc-auth --site-url https://example.com/",
|
|
34
|
+
"",
|
|
35
|
+
"Options:",
|
|
36
|
+
" --site-url <url> Skip the site picker and use this property directly",
|
|
37
|
+
" -h, --help Show this help",
|
|
38
|
+
"",
|
|
39
|
+
`Credentials are written to: ${credentialsFilePath}`,
|
|
40
|
+
""
|
|
41
|
+
].join("\n")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function buildAuthUrl(clientId, redirectUri, state) {
|
|
45
|
+
const params = new URLSearchParams({
|
|
46
|
+
client_id: clientId,
|
|
47
|
+
redirect_uri: redirectUri,
|
|
48
|
+
response_type: "code",
|
|
49
|
+
scope: OAUTH_SCOPE,
|
|
50
|
+
access_type: "offline",
|
|
51
|
+
prompt: "consent",
|
|
52
|
+
state
|
|
53
|
+
});
|
|
54
|
+
return `${OAUTH_AUTH_URL}?${params.toString()}`;
|
|
55
|
+
}
|
|
56
|
+
async function waitForAuthorizationCode(port, expectedState, authUrl) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
let settled = false;
|
|
59
|
+
const finish = (fn) => {
|
|
60
|
+
if (settled) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
fn();
|
|
63
|
+
};
|
|
64
|
+
const server = http.createServer((req, res) => {
|
|
65
|
+
if (!req.url) {
|
|
66
|
+
res.writeHead(404).end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const parsed = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
70
|
+
const code = parsed.searchParams.get("code");
|
|
71
|
+
const state = parsed.searchParams.get("state");
|
|
72
|
+
const error = parsed.searchParams.get("error");
|
|
73
|
+
if (error) {
|
|
74
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
75
|
+
res.end(renderPage("Authorization was denied", `Google returned: ${escapeHtml(error)}. Close this tab and re-run the command.`));
|
|
76
|
+
finish(() => {
|
|
77
|
+
server.close();
|
|
78
|
+
reject(new GscAuthError(`OAuth denied: ${error}`));
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!code) {
|
|
83
|
+
res.writeHead(204).end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (state !== expectedState) {
|
|
87
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
88
|
+
res.end(renderPage("Security check failed", "The state parameter did not match. Please re-run the command."));
|
|
89
|
+
finish(() => {
|
|
90
|
+
server.close();
|
|
91
|
+
reject(new GscAuthError("OAuth state mismatch"));
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
96
|
+
res.end(renderPage("Signed in successfully", "You can close this tab and return to the terminal."));
|
|
97
|
+
finish(() => {
|
|
98
|
+
setTimeout(() => server.close(), 200);
|
|
99
|
+
resolve({ code, state });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
server.on("error", (err) => {
|
|
103
|
+
finish(() => reject(new Error(`Loopback server failed: ${err.message}`)));
|
|
104
|
+
});
|
|
105
|
+
server.listen(port, "127.0.0.1", () => {
|
|
106
|
+
process.stderr.write(`
|
|
107
|
+
Opening your browser to sign in with Google...
|
|
108
|
+
`);
|
|
109
|
+
process.stderr.write(`If it doesn't open automatically, visit:
|
|
110
|
+
${authUrl}
|
|
111
|
+
|
|
112
|
+
`);
|
|
113
|
+
openBrowser(authUrl).catch((err) => {
|
|
114
|
+
logger.warn({ err: err.message }, "openBrowser failed");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
finish(() => {
|
|
119
|
+
server.close();
|
|
120
|
+
reject(new Error("Timed out waiting for OAuth callback (5 minutes)."));
|
|
121
|
+
});
|
|
122
|
+
}, 5 * 60 * 1e3);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function renderPage(title, body) {
|
|
126
|
+
return `<!DOCTYPE html>
|
|
127
|
+
<html lang="en">
|
|
128
|
+
<head>
|
|
129
|
+
<meta charset="utf-8"><title>${escapeHtml(title)}</title>
|
|
130
|
+
<style>body{font:15px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#222}h1{font-size:22px;margin-bottom:12px}p{line-height:1.5}</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></body>
|
|
133
|
+
</html>`;
|
|
134
|
+
}
|
|
135
|
+
function escapeHtml(s) {
|
|
136
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
137
|
+
}
|
|
138
|
+
async function exchangeCodeForTokens(code, clientId, clientSecret, redirectUri) {
|
|
139
|
+
return withResilience(async () => {
|
|
140
|
+
const body = new URLSearchParams({
|
|
141
|
+
code,
|
|
142
|
+
client_id: clientId,
|
|
143
|
+
client_secret: clientSecret,
|
|
144
|
+
redirect_uri: redirectUri,
|
|
145
|
+
grant_type: "authorization_code"
|
|
146
|
+
});
|
|
147
|
+
const res = await fetch(OAUTH_TOKEN_URL, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
150
|
+
body: body.toString()
|
|
151
|
+
});
|
|
152
|
+
const json = await res.json();
|
|
153
|
+
if (!res.ok || json.error) {
|
|
154
|
+
const err = new Error(
|
|
155
|
+
`Token exchange failed: ${json.error_description || json.error || res.statusText}`
|
|
156
|
+
);
|
|
157
|
+
err.status = res.status;
|
|
158
|
+
err.code = res.status;
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
return json;
|
|
162
|
+
}, "oauth.exchangeCode");
|
|
163
|
+
}
|
|
164
|
+
async function enumerateSites(accessToken) {
|
|
165
|
+
const oauth2Client = new google.auth.OAuth2();
|
|
166
|
+
oauth2Client.setCredentials({ access_token: accessToken });
|
|
167
|
+
const svc = google.searchconsole({ version: "v1", auth: oauth2Client });
|
|
168
|
+
const resp = await withResilience(
|
|
169
|
+
() => svc.sites.list(),
|
|
170
|
+
"auth.listSites"
|
|
171
|
+
);
|
|
172
|
+
const sites = (resp.data.siteEntry || []).map((entry) => ({
|
|
173
|
+
siteUrl: entry.siteUrl || "",
|
|
174
|
+
permissionLevel: entry.permissionLevel || ""
|
|
175
|
+
}));
|
|
176
|
+
if (sites.length === 0) {
|
|
177
|
+
throw new GscAuthError(
|
|
178
|
+
"No Search Console properties found for this Google account. Make sure you signed in with an account that has access to at least one property."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return sites;
|
|
182
|
+
}
|
|
183
|
+
async function pickSite(sites, presetSiteUrl) {
|
|
184
|
+
if (presetSiteUrl) {
|
|
185
|
+
const match = sites.find((s) => s.siteUrl === presetSiteUrl);
|
|
186
|
+
if (!match) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`--site-url "${presetSiteUrl}" was not found among ${sites.length} accessible properties. Remove the flag to pick interactively.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return match;
|
|
192
|
+
}
|
|
193
|
+
if (sites.length === 1) {
|
|
194
|
+
process.stderr.write(`
|
|
195
|
+
Only one property accessible: ${sites[0].siteUrl}. Auto-selecting.
|
|
196
|
+
`);
|
|
197
|
+
return sites[0];
|
|
198
|
+
}
|
|
199
|
+
const choices = sites.map((site) => ({
|
|
200
|
+
title: `${site.siteUrl} (${site.permissionLevel})`,
|
|
201
|
+
value: site
|
|
202
|
+
}));
|
|
203
|
+
const response = await prompts(
|
|
204
|
+
{
|
|
205
|
+
type: "select",
|
|
206
|
+
name: "site",
|
|
207
|
+
message: "Which Search Console property should Claude use by default?",
|
|
208
|
+
choices,
|
|
209
|
+
initial: 0
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
onCancel: () => {
|
|
213
|
+
throw new Error("Cancelled by user");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
if (!response.site) throw new Error("No site selected");
|
|
218
|
+
return response.site;
|
|
219
|
+
}
|
|
220
|
+
async function run(argv = process.argv.slice(2)) {
|
|
221
|
+
const args = parseArgs(argv);
|
|
222
|
+
if (args.help) {
|
|
223
|
+
printHelp();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const clientId = process.env.GOOGLE_GSC_CLIENT_ID?.trim() || EMBEDDED_CLIENT_ID;
|
|
227
|
+
const clientSecret = process.env.GOOGLE_GSC_CLIENT_SECRET?.trim() || EMBEDDED_CLIENT_SECRET;
|
|
228
|
+
if (!clientId || !clientSecret) {
|
|
229
|
+
process.stderr.write(
|
|
230
|
+
"This build of mcp-gsc was published without embedded OAuth credentials.\nSet GOOGLE_GSC_CLIENT_ID and GOOGLE_GSC_CLIENT_SECRET in your environment.\n"
|
|
231
|
+
);
|
|
232
|
+
process.exit(2);
|
|
233
|
+
}
|
|
234
|
+
const port = await findFreeLoopbackPort();
|
|
235
|
+
const redirectUri = `http://127.0.0.1:${port}`;
|
|
236
|
+
const state = randomState();
|
|
237
|
+
const authUrl = buildAuthUrl(clientId, redirectUri, state);
|
|
238
|
+
process.stderr.write("\n=== mcp-gsc authentication ===\n");
|
|
239
|
+
const { code } = await waitForAuthorizationCode(port, state, authUrl);
|
|
240
|
+
process.stderr.write("Authorization code received. Exchanging for tokens...\n");
|
|
241
|
+
const tokens = await exchangeCodeForTokens(code, clientId, clientSecret, redirectUri);
|
|
242
|
+
if (!tokens.refresh_token) {
|
|
243
|
+
throw new GscAuthError(
|
|
244
|
+
"Google did not return a refresh token. This can happen if you previously granted consent to this app. Revoke access at https://myaccount.google.com/permissions and try again."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
process.stderr.write("Tokens received. Fetching accessible Search Console properties...\n");
|
|
248
|
+
const sites = await enumerateSites(tokens.access_token);
|
|
249
|
+
const chosen = await pickSite(sites, args.siteUrl);
|
|
250
|
+
const stored = {
|
|
251
|
+
version: CREDENTIALS_FILE_VERSION,
|
|
252
|
+
refresh_token: tokens.refresh_token,
|
|
253
|
+
site_urls: sites.map((s) => s.siteUrl),
|
|
254
|
+
primary_site_url: chosen.siteUrl,
|
|
255
|
+
obtained_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
256
|
+
scopes: [OAUTH_SCOPE]
|
|
257
|
+
};
|
|
258
|
+
writeStoredCredentials(stored);
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
[
|
|
261
|
+
"",
|
|
262
|
+
"Done.",
|
|
263
|
+
"",
|
|
264
|
+
` Property: ${chosen.siteUrl}`,
|
|
265
|
+
` Permission: ${chosen.permissionLevel}`,
|
|
266
|
+
` Saved to: ${credentialsFilePath}`,
|
|
267
|
+
"",
|
|
268
|
+
"Next step: fully quit Claude Desktop (Cmd+Q / File > Exit) and reopen it.",
|
|
269
|
+
'Then try: "List my Search Console properties"',
|
|
270
|
+
""
|
|
271
|
+
].join("\n")
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
function randomState() {
|
|
275
|
+
const bytes = new Uint8Array(16);
|
|
276
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
277
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
278
|
+
}
|
|
279
|
+
const isMain = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/auth-cli.js") || process.argv[1]?.endsWith("\\auth-cli.js");
|
|
280
|
+
if (isMain) {
|
|
281
|
+
run().catch((err) => {
|
|
282
|
+
const classified = classifyError(err);
|
|
283
|
+
process.stderr.write(`
|
|
284
|
+
${classified.message}
|
|
285
|
+
`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
export {
|
|
290
|
+
run
|
|
291
|
+
};
|
|
292
|
+
//# sourceMappingURL=auth-cli.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/auth-cli.ts"],
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n// ============================================\n// mcp-gsc-auth -- one-time OAuth + site selection\n// ============================================\n// Flow:\n// 1. Loopback HTTP listener on a free port\n// 2. Open browser to Google OAuth consent screen (webmasters.readonly scope)\n// 3. Exchange code for tokens\n// 4. Enumerate accessible GSC sites\n// 5. User picks which site to use as default\n// 6. Write credentials to ~/.config/mcp-gsc-nodejs/credentials.json\n\nimport { google } from \"googleapis\";\nimport http from \"http\";\nimport promptsImport from \"prompts\";\nimport { URL } from \"url\";\nimport { writeStoredCredentials, credentialsFilePath, CREDENTIALS_FILE_VERSION, type StoredCredentials } from \"./credentials.js\";\nimport { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from \"./embedded-secrets.js\";\nimport { classifyError, GscAuthError } from \"./errors.js\";\nimport { findFreeLoopbackPort, openBrowser } from \"./platform.js\";\nimport { logger, withResilience } from \"./resilience.js\";\n\nconst prompts = (promptsImport as unknown as { default?: typeof promptsImport }).default ?? promptsImport;\n\nconst OAUTH_SCOPE = \"https://www.googleapis.com/auth/webmasters.readonly\";\nconst OAUTH_AUTH_URL = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst OAUTH_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\n\ninterface CliArgs {\n siteUrl?: string;\n help: boolean;\n}\n\nfunction parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = { help: false };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n if (a === \"--help\" || a === \"-h\") args.help = true;\n else if (a === \"--site-url\" && argv[i + 1]) {\n args.siteUrl = argv[++i];\n }\n }\n return args;\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n \"mcp-gsc-auth -- authorize Claude to access your Google Search Console data\",\n \"\",\n \"Usage:\",\n \" npx mcp-gsc-auth\",\n \" npx mcp-gsc-auth --site-url https://example.com/\",\n \"\",\n \"Options:\",\n \" --site-url <url> Skip the site picker and use this property directly\",\n \" -h, --help Show this help\",\n \"\",\n `Credentials are written to: ${credentialsFilePath}`,\n \"\",\n ].join(\"\\n\"),\n );\n}\n\n// ============================================\n// OAUTH: LOOPBACK REDIRECT FLOW\n// ============================================\n\nfunction buildAuthUrl(clientId: string, redirectUri: string, state: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: OAUTH_SCOPE,\n access_type: \"offline\",\n prompt: \"consent\",\n state,\n });\n return `${OAUTH_AUTH_URL}?${params.toString()}`;\n}\n\ninterface AuthorizationCode {\n code: string;\n state: string;\n}\n\nasync function waitForAuthorizationCode(\n port: number,\n expectedState: string,\n authUrl: string,\n): Promise<AuthorizationCode> {\n return new Promise<AuthorizationCode>((resolve, reject) => {\n let settled = false;\n const finish = (fn: () => void) => {\n if (settled) return;\n settled = true;\n fn();\n };\n\n const server = http.createServer((req, res) => {\n if (!req.url) {\n res.writeHead(404).end();\n return;\n }\n const parsed = new URL(req.url, `http://127.0.0.1:${port}`);\n const code = parsed.searchParams.get(\"code\");\n const state = parsed.searchParams.get(\"state\");\n const error = parsed.searchParams.get(\"error\");\n\n if (error) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderPage(\"Authorization was denied\", `Google returned: ${escapeHtml(error)}. Close this tab and re-run the command.`));\n finish(() => { server.close(); reject(new GscAuthError(`OAuth denied: ${error}`)); });\n return;\n }\n\n if (!code) {\n res.writeHead(204).end();\n return;\n }\n\n if (state !== expectedState) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderPage(\"Security check failed\", \"The state parameter did not match. Please re-run the command.\"));\n finish(() => { server.close(); reject(new GscAuthError(\"OAuth state mismatch\")); });\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderPage(\"Signed in successfully\", \"You can close this tab and return to the terminal.\"));\n finish(() => {\n setTimeout(() => server.close(), 200);\n resolve({ code, state });\n });\n });\n\n server.on(\"error\", (err) => {\n finish(() => reject(new Error(`Loopback server failed: ${err.message}`)));\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n process.stderr.write(`\\nOpening your browser to sign in with Google...\\n`);\n process.stderr.write(`If it doesn't open automatically, visit:\\n ${authUrl}\\n\\n`);\n openBrowser(authUrl).catch((err) => {\n logger.warn({ err: err.message }, \"openBrowser failed\");\n });\n });\n\n setTimeout(() => {\n finish(() => { server.close(); reject(new Error(\"Timed out waiting for OAuth callback (5 minutes).\")); });\n }, 5 * 60 * 1000);\n });\n}\n\nfunction renderPage(title: string, body: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"><title>${escapeHtml(title)}</title>\n <style>body{font:15px -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#222}h1{font-size:22px;margin-bottom:12px}p{line-height:1.5}</style>\n</head>\n<body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></body>\n</html>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" })[c]!);\n}\n\n// ============================================\n// TOKEN EXCHANGE\n// ============================================\n\ninterface TokenResponse {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n scope: string;\n token_type: string;\n}\n\nasync function exchangeCodeForTokens(\n code: string,\n clientId: string,\n clientSecret: string,\n redirectUri: string,\n): Promise<TokenResponse> {\n return withResilience(async () => {\n const body = new URLSearchParams({\n code,\n client_id: clientId,\n client_secret: clientSecret,\n redirect_uri: redirectUri,\n grant_type: \"authorization_code\",\n });\n const res = await fetch(OAUTH_TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n const json = (await res.json()) as Record<string, unknown>;\n if (!res.ok || json.error) {\n const err = new Error(\n `Token exchange failed: ${json.error_description || json.error || res.statusText}`,\n );\n (err as any).status = res.status;\n (err as any).code = res.status;\n throw err;\n }\n return json as unknown as TokenResponse;\n }, \"oauth.exchangeCode\");\n}\n\n// ============================================\n// SITE ENUMERATION\n// ============================================\n\ninterface GscSite {\n siteUrl: string;\n permissionLevel: string;\n}\n\nasync function enumerateSites(accessToken: string): Promise<GscSite[]> {\n const oauth2Client = new google.auth.OAuth2();\n oauth2Client.setCredentials({ access_token: accessToken });\n\n const svc = google.searchconsole({ version: \"v1\", auth: oauth2Client });\n const resp = await withResilience(\n () => svc.sites.list(),\n \"auth.listSites\",\n );\n\n const sites = (resp.data.siteEntry || []).map((entry) => ({\n siteUrl: entry.siteUrl || \"\",\n permissionLevel: entry.permissionLevel || \"\",\n }));\n\n if (sites.length === 0) {\n throw new GscAuthError(\n \"No Search Console properties found for this Google account. \" +\n \"Make sure you signed in with an account that has access to at least one property.\",\n );\n }\n\n return sites;\n}\n\n// ============================================\n// PICKER\n// ============================================\n\nasync function pickSite(sites: GscSite[], presetSiteUrl?: string): Promise<GscSite> {\n if (presetSiteUrl) {\n const match = sites.find((s) => s.siteUrl === presetSiteUrl);\n if (!match) {\n throw new Error(\n `--site-url \"${presetSiteUrl}\" was not found among ${sites.length} accessible properties. ` +\n `Remove the flag to pick interactively.`,\n );\n }\n return match;\n }\n\n if (sites.length === 1) {\n process.stderr.write(`\\nOnly one property accessible: ${sites[0].siteUrl}. Auto-selecting.\\n`);\n return sites[0];\n }\n\n const choices = sites.map((site) => ({\n title: `${site.siteUrl} (${site.permissionLevel})`,\n value: site,\n }));\n\n const response = await prompts(\n {\n type: \"select\",\n name: \"site\",\n message: \"Which Search Console property should Claude use by default?\",\n choices,\n initial: 0,\n },\n {\n onCancel: () => { throw new Error(\"Cancelled by user\"); },\n },\n );\n\n if (!response.site) throw new Error(\"No site selected\");\n return response.site as GscSite;\n}\n\n// ============================================\n// MAIN\n// ============================================\n\nexport async function run(argv: string[] = process.argv.slice(2)): Promise<void> {\n const args = parseArgs(argv);\n if (args.help) {\n printHelp();\n return;\n }\n\n const clientId = process.env.GOOGLE_GSC_CLIENT_ID?.trim() || EMBEDDED_CLIENT_ID;\n const clientSecret = process.env.GOOGLE_GSC_CLIENT_SECRET?.trim() || EMBEDDED_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n process.stderr.write(\n \"This build of mcp-gsc was published without embedded OAuth credentials.\\n\" +\n \"Set GOOGLE_GSC_CLIENT_ID and GOOGLE_GSC_CLIENT_SECRET in your environment.\\n\",\n );\n process.exit(2);\n }\n\n const port = await findFreeLoopbackPort();\n const redirectUri = `http://127.0.0.1:${port}`;\n const state = randomState();\n const authUrl = buildAuthUrl(clientId, redirectUri, state);\n\n process.stderr.write(\"\\n=== mcp-gsc authentication ===\\n\");\n\n const { code } = await waitForAuthorizationCode(port, state, authUrl);\n process.stderr.write(\"Authorization code received. Exchanging for tokens...\\n\");\n\n const tokens = await exchangeCodeForTokens(code, clientId, clientSecret, redirectUri);\n if (!tokens.refresh_token) {\n throw new GscAuthError(\n \"Google did not return a refresh token. This can happen if you previously granted consent \" +\n \"to this app. Revoke access at https://myaccount.google.com/permissions and try again.\",\n );\n }\n process.stderr.write(\"Tokens received. Fetching accessible Search Console properties...\\n\");\n\n const sites = await enumerateSites(tokens.access_token);\n const chosen = await pickSite(sites, args.siteUrl);\n\n const stored: StoredCredentials = {\n version: CREDENTIALS_FILE_VERSION,\n refresh_token: tokens.refresh_token,\n site_urls: sites.map((s) => s.siteUrl),\n primary_site_url: chosen.siteUrl,\n obtained_at: new Date().toISOString(),\n scopes: [OAUTH_SCOPE],\n };\n writeStoredCredentials(stored);\n\n process.stderr.write(\n [\n \"\",\n \"Done.\",\n \"\",\n ` Property: ${chosen.siteUrl}`,\n ` Permission: ${chosen.permissionLevel}`,\n ` Saved to: ${credentialsFilePath}`,\n \"\",\n \"Next step: fully quit Claude Desktop (Cmd+Q / File > Exit) and reopen it.\",\n 'Then try: \"List my Search Console properties\"',\n \"\",\n ].join(\"\\n\"),\n );\n}\n\nfunction randomState(): string {\n const bytes = new Uint8Array(16);\n (globalThis.crypto as Crypto).getRandomValues(bytes);\n return Array.from(bytes).map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// ============================================\n// ENTRY\n// ============================================\n\nconst isMain =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith(\"/auth-cli.js\") ||\n process.argv[1]?.endsWith(\"\\\\auth-cli.js\");\n\nif (isMain) {\n run().catch((err) => {\n const classified = classifyError(err);\n process.stderr.write(`\\n${classified.message}\\n`);\n process.exit(1);\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const CREDENTIALS_FILE_VERSION = 1;
|
|
2
|
+
export interface StoredCredentials {
|
|
3
|
+
version: number;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
site_urls: string[];
|
|
6
|
+
primary_site_url?: string;
|
|
7
|
+
obtained_at: string;
|
|
8
|
+
scopes: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedOAuthCredentials {
|
|
11
|
+
client_id: string;
|
|
12
|
+
client_secret: string;
|
|
13
|
+
refresh_token: string;
|
|
14
|
+
site_urls: string[];
|
|
15
|
+
primary_site_url: string;
|
|
16
|
+
source: "env" | "file" | "mixed";
|
|
17
|
+
}
|
|
18
|
+
export declare function readStoredCredentials(filePath?: string): StoredCredentials | null;
|
|
19
|
+
export declare function writeStoredCredentials(creds: StoredCredentials, filePath?: string): void;
|
|
20
|
+
export declare function resolveOAuthCredentials(credsFilePath?: string): ResolvedOAuthCredentials;
|
|
21
|
+
export { configDir, credentialsFilePath } from "./platform.js";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from "./embedded-secrets.js";
|
|
4
|
+
import { credentialsFilePath } from "./platform.js";
|
|
5
|
+
import { logger } from "./resilience.js";
|
|
6
|
+
const CREDENTIALS_FILE_VERSION = 1;
|
|
7
|
+
const envTrimmed = (key) => (process.env[key] || "").trim().replace(/^["']|["']$/g, "");
|
|
8
|
+
function readStoredCredentials(filePath = credentialsFilePath) {
|
|
9
|
+
if (!existsSync(filePath)) return null;
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (parsed.version !== CREDENTIALS_FILE_VERSION) {
|
|
14
|
+
logger.warn(
|
|
15
|
+
{ path: filePath, version: parsed.version, expected: CREDENTIALS_FILE_VERSION },
|
|
16
|
+
"Credentials file version mismatch"
|
|
17
|
+
);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
logger.warn({ err, path: filePath }, "Failed to parse credentials file");
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeStoredCredentials(creds, filePath = credentialsFilePath) {
|
|
27
|
+
const dir = path.dirname(filePath);
|
|
28
|
+
if (!existsSync(dir)) {
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(filePath, JSON.stringify(creds, null, 2), { encoding: "utf-8" });
|
|
32
|
+
try {
|
|
33
|
+
chmodSync(filePath, 384);
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function resolveOAuthCredentials(credsFilePath = credentialsFilePath) {
|
|
38
|
+
const client_id = envTrimmed("GOOGLE_GSC_CLIENT_ID") || EMBEDDED_CLIENT_ID;
|
|
39
|
+
const client_secret = envTrimmed("GOOGLE_GSC_CLIENT_SECRET") || EMBEDDED_CLIENT_SECRET;
|
|
40
|
+
const stored = readStoredCredentials(credsFilePath);
|
|
41
|
+
const envRefresh = envTrimmed("GOOGLE_GSC_REFRESH_TOKEN");
|
|
42
|
+
const envSiteUrl = envTrimmed("GOOGLE_GSC_SITE_URL");
|
|
43
|
+
const refresh_token = envRefresh || stored?.refresh_token || "";
|
|
44
|
+
const site_urls = stored?.site_urls || [];
|
|
45
|
+
const primary_site_url = envSiteUrl || stored?.primary_site_url || site_urls[0] || "";
|
|
46
|
+
const source = envRefresh && stored ? "mixed" : envRefresh ? "env" : stored ? "file" : "env";
|
|
47
|
+
const missing = [];
|
|
48
|
+
if (!client_id) missing.push("client_id");
|
|
49
|
+
if (!client_secret) missing.push("client_secret");
|
|
50
|
+
if (!refresh_token) missing.push("refresh_token");
|
|
51
|
+
if (missing.length > 0) {
|
|
52
|
+
throw new Error(buildMissingCredentialsMessage(missing, Boolean(stored)));
|
|
53
|
+
}
|
|
54
|
+
return { client_id, client_secret, refresh_token, site_urls, primary_site_url, source };
|
|
55
|
+
}
|
|
56
|
+
function buildMissingCredentialsMessage(missing, hasFile) {
|
|
57
|
+
const runAuth = "npx mcp-gsc-auth";
|
|
58
|
+
const lines = [
|
|
59
|
+
`Missing GSC OAuth credentials: ${missing.join(", ")}.`,
|
|
60
|
+
``,
|
|
61
|
+
`To get started, run:`,
|
|
62
|
+
` ${runAuth}`,
|
|
63
|
+
``,
|
|
64
|
+
`This will open your browser, walk you through Google sign-in, let you pick which`,
|
|
65
|
+
`Search Console property to use, and save the result to:`,
|
|
66
|
+
` ${credentialsFilePath}`
|
|
67
|
+
];
|
|
68
|
+
if (hasFile) {
|
|
69
|
+
lines.push(
|
|
70
|
+
``,
|
|
71
|
+
`A credentials file exists at ${credentialsFilePath} but is missing required fields.`,
|
|
72
|
+
`Re-run the auth helper to refresh it.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
lines.push(
|
|
76
|
+
``,
|
|
77
|
+
`Advanced: you can bypass the auth helper by setting these env vars:`,
|
|
78
|
+
` GOOGLE_GSC_CLIENT_ID, GOOGLE_GSC_CLIENT_SECRET, GOOGLE_GSC_REFRESH_TOKEN`,
|
|
79
|
+
``,
|
|
80
|
+
`Or use a service account: set GOOGLE_APPLICATION_CREDENTIALS to a JSON key file.`
|
|
81
|
+
);
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
import { configDir as configDir2, credentialsFilePath as credentialsFilePath2 } from "./platform.js";
|
|
85
|
+
export {
|
|
86
|
+
CREDENTIALS_FILE_VERSION,
|
|
87
|
+
configDir2 as configDir,
|
|
88
|
+
credentialsFilePath2 as credentialsFilePath,
|
|
89
|
+
readStoredCredentials,
|
|
90
|
+
resolveOAuthCredentials,
|
|
91
|
+
writeStoredCredentials
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/credentials.ts"],
|
|
4
|
+
"sourcesContent": ["// ============================================\n// CREDENTIAL LOADING & PERSISTENCE\n// ============================================\n// Priority order:\n// 1. config.json + service account (existing multi-client setups)\n// 2. GOOGLE_GSC_* env vars (explicit override)\n// 3. Per-user OAuth credentials file (written by mcp-gsc-auth)\n// 4. EMBEDDED_* constants (client_id, client_secret from build-time)\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from \"fs\";\nimport path from \"path\";\nimport { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from \"./embedded-secrets.js\";\nimport { configDir, credentialsFilePath } from \"./platform.js\";\nimport { logger } from \"./resilience.js\";\n\nexport const CREDENTIALS_FILE_VERSION = 1;\n\nexport interface StoredCredentials {\n version: number;\n refresh_token: string;\n site_urls: string[];\n primary_site_url?: string;\n obtained_at: string;\n scopes: string[];\n}\n\nexport interface ResolvedOAuthCredentials {\n client_id: string;\n client_secret: string;\n refresh_token: string;\n site_urls: string[];\n primary_site_url: string;\n source: \"env\" | \"file\" | \"mixed\";\n}\n\nconst envTrimmed = (key: string): string =>\n (process.env[key] || \"\").trim().replace(/^[\"']|[\"']$/g, \"\");\n\n// ============================================\n// FILE I/O\n// ============================================\n\nexport function readStoredCredentials(filePath: string = credentialsFilePath): StoredCredentials | null {\n if (!existsSync(filePath)) return null;\n try {\n const raw = readFileSync(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (parsed.version !== CREDENTIALS_FILE_VERSION) {\n logger.warn(\n { path: filePath, version: parsed.version, expected: CREDENTIALS_FILE_VERSION },\n \"Credentials file version mismatch\",\n );\n return null;\n }\n return parsed as StoredCredentials;\n } catch (err) {\n logger.warn({ err, path: filePath }, \"Failed to parse credentials file\");\n return null;\n }\n}\n\nexport function writeStoredCredentials(\n creds: StoredCredentials,\n filePath: string = credentialsFilePath,\n): void {\n const dir = path.dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, JSON.stringify(creds, null, 2), { encoding: \"utf-8\" });\n try {\n chmodSync(filePath, 0o600);\n } catch {\n // Best-effort on POSIX\n }\n}\n\n// ============================================\n// RESOLVE (read-time priority chain)\n// ============================================\n\nexport function resolveOAuthCredentials(\n credsFilePath: string = credentialsFilePath,\n): ResolvedOAuthCredentials {\n const client_id = envTrimmed(\"GOOGLE_GSC_CLIENT_ID\") || EMBEDDED_CLIENT_ID;\n const client_secret = envTrimmed(\"GOOGLE_GSC_CLIENT_SECRET\") || EMBEDDED_CLIENT_SECRET;\n\n const stored = readStoredCredentials(credsFilePath);\n const envRefresh = envTrimmed(\"GOOGLE_GSC_REFRESH_TOKEN\");\n const envSiteUrl = envTrimmed(\"GOOGLE_GSC_SITE_URL\");\n\n const refresh_token = envRefresh || stored?.refresh_token || \"\";\n const site_urls = stored?.site_urls || [];\n const primary_site_url = envSiteUrl || stored?.primary_site_url || site_urls[0] || \"\";\n\n const source: ResolvedOAuthCredentials[\"source\"] =\n envRefresh && stored ? \"mixed\" : envRefresh ? \"env\" : stored ? \"file\" : \"env\";\n\n const missing: string[] = [];\n if (!client_id) missing.push(\"client_id\");\n if (!client_secret) missing.push(\"client_secret\");\n if (!refresh_token) missing.push(\"refresh_token\");\n\n if (missing.length > 0) {\n throw new Error(buildMissingCredentialsMessage(missing, Boolean(stored)));\n }\n\n return { client_id, client_secret, refresh_token, site_urls, primary_site_url, source };\n}\n\nfunction buildMissingCredentialsMessage(missing: string[], hasFile: boolean): string {\n const runAuth = \"npx mcp-gsc-auth\";\n const lines: string[] = [\n `Missing GSC OAuth credentials: ${missing.join(\", \")}.`,\n ``,\n `To get started, run:`,\n ` ${runAuth}`,\n ``,\n `This will open your browser, walk you through Google sign-in, let you pick which`,\n `Search Console property to use, and save the result to:`,\n ` ${credentialsFilePath}`,\n ];\n if (hasFile) {\n lines.push(\n ``,\n `A credentials file exists at ${credentialsFilePath} but is missing required fields.`,\n `Re-run the auth helper to refresh it.`,\n );\n }\n lines.push(\n ``,\n `Advanced: you can bypass the auth helper by setting these env vars:`,\n ` GOOGLE_GSC_CLIENT_ID, GOOGLE_GSC_CLIENT_SECRET, GOOGLE_GSC_REFRESH_TOKEN`,\n ``,\n `Or use a service account: set GOOGLE_APPLICATION_CREDENTIALS to a JSON key file.`,\n );\n return lines.join(\"\\n\");\n}\n\nexport { configDir, credentialsFilePath } from \"./platform.js\";\n"],
|
|
5
|
+
"mappings": "AASA,SAAS,YAAY,WAAW,cAAc,eAAe,iBAAiB;AAC9E,OAAO,UAAU;AACjB,SAAS,oBAAoB,8BAA8B;AAC3D,SAAoB,2BAA2B;AAC/C,SAAS,cAAc;AAEhB,MAAM,2BAA2B;AAoBxC,MAAM,aAAa,CAAC,SACjB,QAAQ,IAAI,GAAG,KAAK,IAAI,KAAK,EAAE,QAAQ,gBAAgB,EAAE;AAMrD,SAAS,sBAAsB,WAAmB,qBAA+C;AACtG,MAAI,CAAC,WAAW,QAAQ,EAAG,QAAO;AAClC,MAAI;AACF,UAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,YAAY,0BAA0B;AAC/C,aAAO;AAAA,QACL,EAAE,MAAM,UAAU,SAAS,OAAO,SAAS,UAAU,yBAAyB;AAAA,QAC9E;AAAA,MACF;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,KAAK,EAAE,KAAK,MAAM,SAAS,GAAG,kCAAkC;AACvE,WAAO;AAAA,EACT;AACF;AAEO,SAAS,uBACd,OACA,WAAmB,qBACb;AACN,QAAM,MAAM,KAAK,QAAQ,QAAQ;AACjC,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,gBAAc,UAAU,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC7E,MAAI;AACF,cAAU,UAAU,GAAK;AAAA,EAC3B,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,wBACd,gBAAwB,qBACE;AAC1B,QAAM,YAAY,WAAW,sBAAsB,KAAK;AACxD,QAAM,gBAAgB,WAAW,0BAA0B,KAAK;AAEhE,QAAM,SAAS,sBAAsB,aAAa;AAClD,QAAM,aAAa,WAAW,0BAA0B;AACxD,QAAM,aAAa,WAAW,qBAAqB;AAEnD,QAAM,gBAAgB,cAAc,QAAQ,iBAAiB;AAC7D,QAAM,YAAY,QAAQ,aAAa,CAAC;AACxC,QAAM,mBAAmB,cAAc,QAAQ,oBAAoB,UAAU,CAAC,KAAK;AAEnF,QAAM,SACJ,cAAc,SAAS,UAAU,aAAa,QAAQ,SAAS,SAAS;AAE1E,QAAM,UAAoB,CAAC;AAC3B,MAAI,CAAC,UAAW,SAAQ,KAAK,WAAW;AACxC,MAAI,CAAC,cAAe,SAAQ,KAAK,eAAe;AAChD,MAAI,CAAC,cAAe,SAAQ,KAAK,eAAe;AAEhD,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI,MAAM,+BAA+B,SAAS,QAAQ,MAAM,CAAC,CAAC;AAAA,EAC1E;AAEA,SAAO,EAAE,WAAW,eAAe,eAAe,WAAW,kBAAkB,OAAO;AACxF;AAEA,SAAS,+BAA+B,SAAmB,SAA0B;AACnF,QAAM,UAAU;AAChB,QAAM,QAAkB;AAAA,IACtB,kCAAkC,QAAQ,KAAK,IAAI,CAAC;AAAA,IACpD;AAAA,IACA;AAAA,IACA,OAAO,OAAO;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,mBAAmB;AAAA,EAC5B;AACA,MAAI,SAAS;AACX,UAAM;AAAA,MACJ;AAAA,MACA,gCAAgC,mBAAmB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,aAAAA,YAAW,uBAAAC,4BAA2B;",
|
|
6
|
+
"names": ["configDir", "credentialsFilePath"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const EMBEDDED_CLIENT_ID = "557294086068-o7rb5neg65g28uf65j85q0h60cop40j9.apps.googleusercontent.com";
|
|
2
|
+
const EMBEDDED_CLIENT_SECRET = "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";
|
|
3
|
+
function hasEmbeddedSecrets() {
|
|
4
|
+
return EMBEDDED_CLIENT_ID.length > 10 && EMBEDDED_CLIENT_SECRET.length > 10;
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
EMBEDDED_CLIENT_ID,
|
|
8
|
+
EMBEDDED_CLIENT_SECRET,
|
|
9
|
+
hasEmbeddedSecrets
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=embedded-secrets.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/embedded-secrets.ts"],
|
|
4
|
+
"sourcesContent": ["// ============================================\n// BUILD-TIME INJECTED SECRETS\n// ============================================\n// Replaced by esbuild --define at build time. In dev mode (tsx),\n// falls back to runtime env vars via credentials.ts.\n//\n// GSC only needs client_id + client_secret (no developer token).\n// Google Desktop OAuth client ID/secret are NOT true secrets per Google docs.\n\nexport const EMBEDDED_CLIENT_ID: string = process.env.EMBEDDED_CLIENT_ID || \"\";\nexport const EMBEDDED_CLIENT_SECRET: string = process.env.EMBEDDED_CLIENT_SECRET || \"\";\n\nexport function hasEmbeddedSecrets(): boolean {\n return EMBEDDED_CLIENT_ID.length > 10 && EMBEDDED_CLIENT_SECRET.length > 10;\n}\n"],
|
|
5
|
+
"mappings": "AASO,MAAM,qBAA6B;AACnC,MAAM,yBAAiC;AAEvC,SAAS,qBAA8B;AAC5C,SAAO,mBAAmB,SAAS,MAAM,uBAAuB,SAAS;AAC3E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -1,65 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
super(message);
|
|
8
|
-
this.cause = cause;
|
|
9
|
-
this.name = "GscAuthError";
|
|
10
|
-
}
|
|
1
|
+
class GscAuthError extends Error {
|
|
2
|
+
constructor(message, cause) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.cause = cause;
|
|
5
|
+
this.name = "GscAuthError";
|
|
6
|
+
}
|
|
11
7
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
8
|
+
class GscRateLimitError extends Error {
|
|
9
|
+
constructor(retryAfterMs, cause) {
|
|
10
|
+
super(`GSC rate limited, retry after ${retryAfterMs}ms`);
|
|
11
|
+
this.retryAfterMs = retryAfterMs;
|
|
12
|
+
this.name = "GscRateLimitError";
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
}
|
|
20
15
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
16
|
+
class GscServiceError extends Error {
|
|
17
|
+
constructor(message, cause) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
this.name = "GscServiceError";
|
|
21
|
+
}
|
|
28
22
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else if (credentialsFile.trim().length > 0 && credentialsFile.trim().length < 5) {
|
|
38
|
-
// Basic format validation: path should have reasonable length > 5 chars
|
|
39
|
-
missing.push("credentials_file (format: path too short, expected length > 5)");
|
|
40
|
-
}
|
|
41
|
-
return { valid: missing.length === 0, missing };
|
|
23
|
+
function validateCredentials(credentialsFile) {
|
|
24
|
+
const missing = [];
|
|
25
|
+
if (!credentialsFile || credentialsFile.trim() === "") {
|
|
26
|
+
missing.push("credentials_file (in config.json or GOOGLE_APPLICATION_CREDENTIALS env var)");
|
|
27
|
+
} else if (credentialsFile.trim().length > 0 && credentialsFile.trim().length < 5) {
|
|
28
|
+
missing.push("credentials_file (format: path too short, expected length > 5)");
|
|
29
|
+
}
|
|
30
|
+
return { valid: missing.length === 0, missing };
|
|
42
31
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (status >= 500 || message.includes("INTERNAL") || message.includes("UNAVAILABLE")) {
|
|
62
|
-
return new GscServiceError(`GSC API server error: ${message}`, error);
|
|
63
|
-
}
|
|
64
|
-
return error;
|
|
32
|
+
function classifyError(error) {
|
|
33
|
+
const message = error?.message || String(error);
|
|
34
|
+
const status = error?.code || error?.status;
|
|
35
|
+
const bodyError = error?.response?.body?.error || error?.data?.error || error?.errors?.[0];
|
|
36
|
+
if (status === 401 || status === 403 || message.includes("invalid_grant") || message.includes("PERMISSION_DENIED") || message.includes("access_denied") || message.includes("Invalid credentials") || bodyError?.code === 403) {
|
|
37
|
+
return new GscAuthError(
|
|
38
|
+
`GSC auth failed: ${message}. Check service account credentials and permissions.`,
|
|
39
|
+
error
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (status === 429 || message.includes("rateLimitExceeded") || message.includes("RESOURCE_EXHAUSTED")) {
|
|
43
|
+
const retryMs = 6e4;
|
|
44
|
+
return new GscRateLimitError(retryMs, error);
|
|
45
|
+
}
|
|
46
|
+
if (status >= 500 || message.includes("INTERNAL") || message.includes("UNAVAILABLE")) {
|
|
47
|
+
return new GscServiceError(`GSC API server error: ${message}`, error);
|
|
48
|
+
}
|
|
49
|
+
return error;
|
|
65
50
|
}
|
|
51
|
+
export {
|
|
52
|
+
GscAuthError,
|
|
53
|
+
GscRateLimitError,
|
|
54
|
+
GscServiceError,
|
|
55
|
+
classifyError,
|
|
56
|
+
validateCredentials
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=errors.js.map
|