@gscdump/cli 3.6.3 → 3.8.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/README.md +13 -6
- package/dist/auth-scopes.mjs +4 -4
- package/dist/auth.mjs +39 -2
- package/dist/commands/auth.mjs +3 -7
- package/dist/commands/doctor.mjs +1 -1
- package/dist/commands/init.mjs +4 -7
- package/dist/config.mjs +6 -1
- package/dist/error-handler.mjs +1 -1
- package/dist/hosted-auth.mjs +55 -0
- package/dist/package.mjs +1 -1
- package/package.json +6 -6
- package/skills/gscdump/SKILL.md +7 -4
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx @gscdump/cli
|
|
|
20
20
|
## Quick start
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
#
|
|
23
|
+
# Connect Google for free local CLI use
|
|
24
24
|
gscdump init --mode local
|
|
25
25
|
gscdump auth login --mode local
|
|
26
26
|
|
|
@@ -280,16 +280,22 @@ This section covers local Google credentials. See [shared authentication](#hoste
|
|
|
280
280
|
Credentials are saved under `~/.config/gscdump/` on XDG systems, or the platform equivalent.
|
|
281
281
|
Use `gscdump auth login --mode local` to connect Google and save local mode.
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
If you configure your own Google OAuth client, browser login uses a temporary listener on `127.0.0.1` with a random port.
|
|
284
284
|
Each attempt uses state validation and PKCE S256 to bind the authorization response to that attempt.
|
|
285
285
|
The listener closes after authorization, denial, or a five-minute timeout.
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
By default, login opens gscdump.com. No Google Cloud project is required.
|
|
288
|
+
The platform handles Google login and token refresh. Data queries call Google directly.
|
|
289
|
+
This grants read-only Search Console access. It does not activate hosted sync, storage, or hosted MCP.
|
|
290
|
+
Hosted Pro is free during beta and will require payment after launch.
|
|
291
|
+
For Google write operations, configure your own OAuth client with the required scopes.
|
|
292
|
+
|
|
293
|
+
For your own OAuth client:
|
|
288
294
|
|
|
289
295
|
1. Create a Google Cloud project.
|
|
290
296
|
2. Enable **Search Console API**, **Web Search Indexing API**, and **Site Verification API**.
|
|
291
297
|
3. Create OAuth2 credentials (Desktop app).
|
|
292
|
-
4.
|
|
298
|
+
4. Set `GSC_CLIENT_ID` and `GSC_CLIENT_SECRET` for your Desktop app.
|
|
293
299
|
5. Run `gscdump auth login --mode local` to save local mode.
|
|
294
300
|
|
|
295
301
|
### BYOK (Bring Your Own Key)
|
|
@@ -328,8 +334,9 @@ gscdump auth login --mode local --no-browser
|
|
|
328
334
|
# Open the printed URL in your browser.
|
|
329
335
|
```
|
|
330
336
|
|
|
331
|
-
|
|
332
|
-
|
|
337
|
+
Default login works from a remote terminal without port forwarding. Keep the command running until you approve the browser request.
|
|
338
|
+
|
|
339
|
+
With your own OAuth client, login uses the Desktop application loopback flow.
|
|
333
340
|
|
|
334
341
|
If the CLI runs on another host, forward its printed loopback port before opening the URL.
|
|
335
342
|
Keep the login command running on that host.
|
package/dist/auth-scopes.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { GSC_INDEXING_SCOPE, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, hasGoogleScope } from "gscdump/client";
|
|
2
|
-
function missingRequiredScopes(scopes) {
|
|
3
|
-
return [
|
|
1
|
+
import { GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, hasGoogleScope } from "gscdump/client";
|
|
2
|
+
function missingRequiredScopes(scopes, provider) {
|
|
3
|
+
return (provider === "gscdump" ? [GSC_READ_SCOPE] : [
|
|
4
4
|
GSC_WRITE_SCOPE,
|
|
5
5
|
GSC_INDEXING_SCOPE,
|
|
6
6
|
GSC_SITE_VERIFICATION_SCOPE
|
|
7
|
-
].filter((scope) => !hasGoogleScope(scopes, scope));
|
|
7
|
+
]).filter((scope) => !hasGoogleScope(scopes, scope));
|
|
8
8
|
}
|
|
9
9
|
export { missingRequiredScopes };
|
package/dist/auth.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { getConfigDir, loadConfig } from "./config.mjs";
|
|
|
2
2
|
import { pickCliEnvironmentValue, resolveCliEnvironment } from "./environment.mjs";
|
|
3
3
|
import { getAppliedEnvKeys, getLoadedEnvPath } from "./env-file.mjs";
|
|
4
4
|
import { displayPath, logger } from "./utils.mjs";
|
|
5
|
+
import { loginWithPlatform, refreshWithPlatform } from "./hosted-auth.mjs";
|
|
5
6
|
import process from "node:process";
|
|
6
7
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
7
8
|
import fs from "node:fs/promises";
|
|
@@ -11,6 +12,7 @@ import { err, ok, unwrapResult } from "gscdump/result";
|
|
|
11
12
|
import { text } from "@clack/prompts";
|
|
12
13
|
import { createAuth } from "gscdump/client";
|
|
13
14
|
import { createServer } from "node:http";
|
|
15
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
16
|
import { CodeChallengeMethod, JWT, OAuth2Client } from "google-auth-library";
|
|
15
17
|
import open from "open";
|
|
16
18
|
function authErrorToException(error) {
|
|
@@ -261,7 +263,8 @@ async function authenticate(credentials, interactive, opts = {}) {
|
|
|
261
263
|
}
|
|
262
264
|
return oauth2Client;
|
|
263
265
|
}
|
|
264
|
-
const
|
|
266
|
+
const savedTokens = !opts.force ? await loadTokens() : null;
|
|
267
|
+
const existingTokens = savedTokens?.provider === "gscdump" ? null : savedTokens;
|
|
265
268
|
let refreshFailed = false;
|
|
266
269
|
let refreshError = null;
|
|
267
270
|
if (existingTokens) {
|
|
@@ -321,10 +324,44 @@ async function authenticate(credentials, interactive, opts = {}) {
|
|
|
321
324
|
}
|
|
322
325
|
async function getAuth(opts = {}) {
|
|
323
326
|
const { interactive = true, noBrowser = false, force = false } = opts;
|
|
324
|
-
|
|
327
|
+
const env = resolveCliEnvironment();
|
|
328
|
+
const config = opts.config ?? await loadConfig();
|
|
329
|
+
if (env.clientId && env.clientSecret || config.clientId && config.clientSecret) return authenticate(await getAuthCredentials(interactive), interactive, {
|
|
325
330
|
noBrowser,
|
|
326
331
|
force
|
|
327
332
|
});
|
|
333
|
+
let tokens = force ? null : await loadTokens();
|
|
334
|
+
if (tokens?.provider !== "gscdump" || !tokens.refresh_token) {
|
|
335
|
+
if (!interactive) throw new Error("Run `gscdump auth login` to connect Google.");
|
|
336
|
+
tokens = await loginWithPlatform({
|
|
337
|
+
force,
|
|
338
|
+
request: fetch,
|
|
339
|
+
now: Date.now,
|
|
340
|
+
wait: setTimeout$1,
|
|
341
|
+
authorize: async (url) => {
|
|
342
|
+
logger.info(`Open this URL to connect Google:\n${url}`);
|
|
343
|
+
if (!noBrowser) await open(url).catch((error) => logger.warn(`Browser could not open: ${error.message}. Open the URL above.`));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
await saveTokens(tokens);
|
|
347
|
+
}
|
|
348
|
+
const refreshToken = tokens.refresh_token;
|
|
349
|
+
const client = new OAuth2Client();
|
|
350
|
+
client.refreshHandler = async () => {
|
|
351
|
+
const refreshed = await refreshWithPlatform(refreshToken);
|
|
352
|
+
await saveTokens({
|
|
353
|
+
provider: "gscdump",
|
|
354
|
+
refresh_token: refreshToken,
|
|
355
|
+
...refreshed
|
|
356
|
+
});
|
|
357
|
+
return refreshed;
|
|
358
|
+
};
|
|
359
|
+
client.setCredentials({
|
|
360
|
+
access_token: tokens.access_token,
|
|
361
|
+
expiry_date: tokens.expiry_date
|
|
362
|
+
});
|
|
363
|
+
await client.getAccessToken();
|
|
364
|
+
return client;
|
|
328
365
|
}
|
|
329
366
|
async function resolveAuth(opts = {}) {
|
|
330
367
|
const sa = await resolveServiceAccount({ path: opts.serviceAccount });
|
package/dist/commands/auth.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { clearAuthentication, getCloudAccount, parseAuthMode, parseAuthenticatio
|
|
|
3
3
|
import { authCommandMeta } from "../command-meta.mjs";
|
|
4
4
|
import { loadConfig, saveConfig } from "../config.mjs";
|
|
5
5
|
import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "../utils.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
|
7
7
|
import { missingRequiredScopes } from "../auth-scopes.mjs";
|
|
8
8
|
import { clearBingCredentials, getBingClient, inspectBingCredentials } from "../bing-auth.mjs";
|
|
9
9
|
import { runSmokeTest } from "./init.mjs";
|
|
@@ -64,7 +64,7 @@ async function resolveLiveAuthState() {
|
|
|
64
64
|
else if (tokens?.access_token) liveToken = tokens.access_token;
|
|
65
65
|
const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
|
|
66
66
|
const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
|
|
67
|
-
const missing = missingRequiredScopes(scopes);
|
|
67
|
+
const missing = missingRequiredScopes(scopes, byok ? void 0 : tokens?.provider);
|
|
68
68
|
return {
|
|
69
69
|
byok,
|
|
70
70
|
tokens,
|
|
@@ -250,15 +250,11 @@ const refreshCommand = defineCommand({
|
|
|
250
250
|
logger.error("No saved refresh token. Run `gscdump auth login`.");
|
|
251
251
|
process.exit(1);
|
|
252
252
|
}
|
|
253
|
-
const credentials = await getAuthCredentials(false).catch((e) => {
|
|
254
|
-
logger.error(`Cannot resolve credentials: ${e.message}`);
|
|
255
|
-
process.exit(1);
|
|
256
|
-
});
|
|
257
253
|
await saveTokens({
|
|
258
254
|
...tokens,
|
|
259
255
|
expiry_date: 1
|
|
260
256
|
});
|
|
261
|
-
if ((await
|
|
257
|
+
if ((await getAuth({ interactive: false }).catch((e) => {
|
|
262
258
|
logger.error(`Refresh failed: ${e.message}`);
|
|
263
259
|
process.exit(1);
|
|
264
260
|
})).credentials?.access_token) logger.success("Token refreshed");
|
package/dist/commands/doctor.mjs
CHANGED
|
@@ -141,7 +141,7 @@ async function checkAuth(envKeys) {
|
|
|
141
141
|
detail: info.email
|
|
142
142
|
});
|
|
143
143
|
const scopes = info.scope ? info.scope.split(/\s+/) : [];
|
|
144
|
-
const missing = missingRequiredScopes(scopes);
|
|
144
|
+
const missing = missingRequiredScopes(scopes, byok ? void 0 : tokens?.provider);
|
|
145
145
|
if (missing.length > 0) checks.push({
|
|
146
146
|
name: "auth.scopes",
|
|
147
147
|
status: "warn",
|
package/dist/commands/init.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { initCommandMeta } from "../command-meta.mjs";
|
|
|
2
2
|
import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
|
|
3
3
|
import { applyCliEnvironment } from "../environment.mjs";
|
|
4
4
|
import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
|
|
5
|
-
import { authenticate,
|
|
5
|
+
import { authenticate, getAuth, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import fs from "node:fs/promises";
|
|
@@ -117,15 +117,12 @@ const initCommand = defineCommand({
|
|
|
117
117
|
console.log(" \x1B[90mGoogle Search Console data extraction CLI\x1B[0m");
|
|
118
118
|
console.log();
|
|
119
119
|
const dataDir = args["no-store"] ? void 0 : await promptDataDir(config.dataDir);
|
|
120
|
-
const credentials = await getAuthCredentials(true);
|
|
121
120
|
await saveConfig({
|
|
122
121
|
...config,
|
|
123
|
-
...dataDir ? { dataDir } : {}
|
|
124
|
-
clientId: credentials.clientId,
|
|
125
|
-
clientSecret: credentials.clientSecret
|
|
122
|
+
...dataDir ? { dataDir } : {}
|
|
126
123
|
});
|
|
127
|
-
await smokeTest(await
|
|
128
|
-
await maybeWriteEnvFile(
|
|
124
|
+
await smokeTest(await getAuth({ interactive: true }));
|
|
125
|
+
if (config.clientId && config.clientSecret) await maybeWriteEnvFile(config.clientId, config.clientSecret);
|
|
129
126
|
console.log();
|
|
130
127
|
logger.success("Setup complete! Run gscdump to get started.");
|
|
131
128
|
}
|
package/dist/config.mjs
CHANGED
|
@@ -27,8 +27,13 @@ const configSchema = z.strictObject({
|
|
|
27
27
|
]).optional(),
|
|
28
28
|
serviceAccountPath: z.string().optional()
|
|
29
29
|
});
|
|
30
|
+
const RETIRED_KEYS = ["mode", "cloudUrl"];
|
|
31
|
+
function dropRetiredKeys(value) {
|
|
32
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
33
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !RETIRED_KEYS.includes(key)));
|
|
34
|
+
}
|
|
30
35
|
function parseConfig(value) {
|
|
31
|
-
const parsed = configSchema.safeParse(value);
|
|
36
|
+
const parsed = configSchema.safeParse(dropRetiredKeys(value));
|
|
32
37
|
if (!parsed.success) {
|
|
33
38
|
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`);
|
|
34
39
|
throw new Error(`Invalid config at ${getConfigPath()}. ${issues.join("; ")}`);
|
package/dist/error-handler.mjs
CHANGED
|
@@ -75,7 +75,7 @@ var LocalStoreUnsupportedError = class extends Error {
|
|
|
75
75
|
tool;
|
|
76
76
|
mode;
|
|
77
77
|
constructor(tool, mode) {
|
|
78
|
-
super(`analysis "${tool}"
|
|
78
|
+
super(mode === "live" ? `The live API cannot run analysis "${tool}". Run gscdump sync, then retry without --live.` : `Local data cannot run analysis "${tool}".`);
|
|
79
79
|
this.name = "LocalStoreUnsupportedError";
|
|
80
80
|
this.tool = tool;
|
|
81
81
|
this.mode = mode;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const ORIGIN = "https://gscdump.com";
|
|
3
|
+
const accessSchema = z.object({
|
|
4
|
+
accessToken: z.string().min(1),
|
|
5
|
+
expiresAt: z.number().int().positive()
|
|
6
|
+
});
|
|
7
|
+
const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal("pending") }), z.object({
|
|
8
|
+
status: z.literal("complete"),
|
|
9
|
+
tokens: accessSchema.extend({ refreshToken: z.string().min(1) })
|
|
10
|
+
})]);
|
|
11
|
+
async function requestJson(request, route, init = {}) {
|
|
12
|
+
const response = await request(`${ORIGIN}/api/cli/auth/${route}`, {
|
|
13
|
+
...init,
|
|
14
|
+
redirect: "error",
|
|
15
|
+
signal: AbortSignal.timeout(3e4)
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
if (response.status === 429 || response.status >= 500) throw new Error("Google authorization is temporarily unavailable. Try again later.");
|
|
19
|
+
throw new Error("Google authorization failed. Run `gscdump auth login --mode local --force` to reconnect.");
|
|
20
|
+
}
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
23
|
+
async function refreshWithPlatform(refreshToken, request = fetch) {
|
|
24
|
+
const result = accessSchema.parse(await requestJson(request, "refresh", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ refreshToken })
|
|
28
|
+
}));
|
|
29
|
+
return {
|
|
30
|
+
access_token: result.accessToken,
|
|
31
|
+
expiry_date: result.expiresAt
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function loginWithPlatform(deps) {
|
|
35
|
+
const init = z.object({
|
|
36
|
+
code: z.string().regex(/^[A-F0-9]{20}$/),
|
|
37
|
+
expiresIn: z.number().int().positive().max(600)
|
|
38
|
+
}).parse(await requestJson(deps.request, "init", { method: "POST" }));
|
|
39
|
+
const deadline = deps.now() + init.expiresIn * 1e3;
|
|
40
|
+
const redirect = `/app/cli/auth?code=${init.code}`;
|
|
41
|
+
const route = deps.force ? `/auth/google?reauth=1&redirect=${encodeURIComponent(redirect)}` : redirect;
|
|
42
|
+
await deps.authorize(`${ORIGIN}${route}`);
|
|
43
|
+
while (deps.now() < deadline) {
|
|
44
|
+
const result = pollSchema.parse(await requestJson(deps.request, `poll?code=${init.code}`));
|
|
45
|
+
if (result.status === "complete") return {
|
|
46
|
+
provider: "gscdump",
|
|
47
|
+
access_token: result.tokens.accessToken,
|
|
48
|
+
refresh_token: result.tokens.refreshToken,
|
|
49
|
+
expiry_date: result.tokens.expiresAt
|
|
50
|
+
};
|
|
51
|
+
await deps.wait(2e3);
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Authorization expired. Run `gscdump auth login` to try again.");
|
|
54
|
+
}
|
|
55
|
+
export { loginWithPlatform, refreshWithPlatform };
|
package/dist/package.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var version = "3.
|
|
1
|
+
var version = "3.8.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.8.0",
|
|
5
5
|
"description": "CLI for Google Search Console and Bing with hosted or local authentication, data exports, and an MCP server",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -44,15 +44,15 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@clack/prompts": "^1.8.0",
|
|
46
46
|
"@duckdb/node-api": "1.5.5-r.4",
|
|
47
|
-
"@gscdump/analysis": "^3.
|
|
48
|
-
"@gscdump/engine": "^3.
|
|
49
|
-
"@gscdump/engine-gsc-api": "^3.
|
|
50
|
-
"@gscdump/sdk": "^3.
|
|
47
|
+
"@gscdump/analysis": "^3.8.0",
|
|
48
|
+
"@gscdump/engine": "^3.8.0",
|
|
49
|
+
"@gscdump/engine-gsc-api": "^3.8.0",
|
|
50
|
+
"@gscdump/sdk": "^3.8.0",
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
52
|
"citty": "^0.2.2",
|
|
53
53
|
"consola": "^3.4.2",
|
|
54
54
|
"google-auth-library": "^11.0.2",
|
|
55
|
-
"gscdump": "^3.
|
|
55
|
+
"gscdump": "^3.8.0",
|
|
56
56
|
"ofetch": "^1.5.1",
|
|
57
57
|
"open": "^11.0.2",
|
|
58
58
|
"sitemapd": "^0.2.2",
|
package/skills/gscdump/SKILL.md
CHANGED
|
@@ -122,10 +122,13 @@ If local Google credentials are missing, use one of these paths:
|
|
|
122
122
|
| Service account | CI with a service-account key that has Site access | `export GOOGLE_APPLICATION_CREDENTIALS=/abs/path/key.json` |
|
|
123
123
|
| Interactive OAuth | A person is present | `gscdump init --mode local` |
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
when a browser
|
|
128
|
-
|
|
125
|
+
Default local login opens gscdump.com for free Google login and token refresh.
|
|
126
|
+
Data queries call Google directly. No Google Cloud project or hosted activation is required.
|
|
127
|
+
Use `gscdump auth login --mode local --no-browser` when a browser runs on another host.
|
|
128
|
+
The default grant is read-only Search Console access.
|
|
129
|
+
For Google write operations, use your own OAuth client with the required scopes.
|
|
130
|
+
Set `GSC_CLIENT_ID` and `GSC_CLIENT_SECRET` to use a Desktop app OAuth client.
|
|
131
|
+
Cloud mode requires hosted access. Pro is free during beta, then paid after launch.
|
|
129
132
|
|
|
130
133
|
`--profile <name>` or `GSCDUMP_PROFILE` isolates the selected mode and Google, Bing, and cloud credentials.
|
|
131
134
|
|