@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 CHANGED
@@ -20,7 +20,7 @@ npx @gscdump/cli
20
20
  ## Quick start
21
21
 
22
22
  ```bash
23
- # Set up local Google OAuth
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
- Local Google browser login uses a temporary listener on `127.0.0.1` with a random port.
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
- For manual setup:
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. Run `gscdump init --mode local` to configure credentials and a Store directory.
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
- This uses the same Desktop application OAuth client and loopback flow as browser login.
332
- Google's device flow does not support the required scopes.
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.
@@ -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 existingTokens = !opts.force ? await loadTokens() : null;
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
- return authenticate(await getAuthCredentials(interactive), interactive, {
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 });
@@ -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 { authenticate, clearTokens, formatAuthProvenance, getAuth, getAuthCredentials, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
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 authenticate(credentials, false).catch((e) => {
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");
@@ -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",
@@ -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, getAuthCredentials, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
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 authenticate(credentials, true));
128
- await maybeWriteEnvFile(credentials.clientId, credentials.clientSecret);
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("; ")}`);
@@ -75,7 +75,7 @@ var LocalStoreUnsupportedError = class extends Error {
75
75
  tool;
76
76
  mode;
77
77
  constructor(tool, mode) {
78
- super(`analysis "${tool}" has no implementation for the ${mode} source`);
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.6.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.6.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.6.3",
48
- "@gscdump/engine": "^3.6.3",
49
- "@gscdump/engine-gsc-api": "^3.6.3",
50
- "@gscdump/sdk": "^3.6.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.6.3",
55
+ "gscdump": "^3.8.0",
56
56
  "ofetch": "^1.5.1",
57
57
  "open": "^11.0.2",
58
58
  "sitemapd": "^0.2.2",
@@ -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
- `init` needs a Google Cloud OAuth client of type Desktop app. Ask the user to
126
- run it; do not guess client credentials. Use `gscdump auth login --mode local --no-browser`
127
- when a browser cannot open.
128
- Run `gscdump auth login --mode local` to save local mode after configuring credentials.
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