@gscdump/cli 4.2.3 → 4.3.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.
@@ -1,23 +1,23 @@
1
1
  import { authCommandMeta } from "../command-meta.mjs";
2
2
  import { useCliRuntime } from "../runtime.mjs";
3
- import { clearAuthentication, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, revokeCloudSession, saveAuthentication } from "../auth-state.mjs";
3
+ import { clearAuthentication, formatHostedSync, getHostedAccount, parseAuthMode, parseAuthentication, resolveAuthentication, revokeHostedSession, saveAuthentication } from "../auth-state.mjs";
4
4
  import { loadConfig, saveConfig } from "../config.mjs";
5
5
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
6
- import { loginWithCloudSession } from "../hosted-auth.mjs";
7
- import { GOOGLE_NOT_CONNECTED, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
6
+ import { ACCESS_NOT_SET_UP, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
8
7
  import { missingRequiredScopes } from "../auth-scopes.mjs";
9
8
  import { clearBingCredentials, getBingClient, inspectBingCredentials } from "../bing-auth.mjs";
9
+ import { loginWithHostedSession } from "../hosted-auth.mjs";
10
10
  import { currentAccessToken, fetchTokenInfo, redactTokens } from "../token-info.mjs";
11
11
  import { runSmokeTest } from "./init.mjs";
12
12
  import { adoptCurrentConfigAsProfile, profileNameFromEmail, resolveActiveProfile } from "./profile.mjs";
13
13
  import process from "node:process";
14
14
  import { defineCommand } from "citty";
15
15
  import path from "node:path";
16
- import { setTimeout } from "node:timers/promises";
17
16
  import open from "open";
17
+ import { setTimeout } from "node:timers/promises";
18
18
  const MODE_ARG = {
19
19
  type: "string",
20
- description: "Authentication mode: cloud or local"
20
+ description: "Access mode: local or hosted"
21
21
  };
22
22
  function applyAuthMode(args) {
23
23
  const mode = parseAuthMode(args.mode);
@@ -25,45 +25,62 @@ function applyAuthMode(args) {
25
25
  }
26
26
  async function requireLocalAuth(args) {
27
27
  applyAuthMode(args);
28
- if ((await resolveAuthentication())._tag === "Cloud") throw new Error("Cloud authentication uses a CLI session. Google OAuth scopes and token refresh require --mode local.");
28
+ if ((await resolveAuthentication())._tag === "Hosted") throw new Error("Google scopes and token refresh apply to Local mode only. Hosted mode uses a gscdump.com CLI session.");
29
29
  }
30
- async function loginCloud(args) {
30
+ const HOSTED_MODE_NOTE = "Hosted mode reads your gscdump.com record. Commands that call Google need Local mode.";
31
+ const HOSTED_COMMANDS = [
32
+ "sites",
33
+ "query",
34
+ "sitemaps current",
35
+ "sitemaps history",
36
+ "sitemaps membership",
37
+ "sitemaps lastmod",
38
+ "sitemaps export",
39
+ "indexing urls",
40
+ "bing login",
41
+ "bing sites",
42
+ "bing status",
43
+ "bing dump",
44
+ "bing inspect",
45
+ "bing verify"
46
+ ];
47
+ async function loginHosted(args) {
31
48
  const env = useCliRuntime().environment;
32
49
  const apiKey = String(args["api-key"] ?? env.GSCDUMP_API_KEY ?? "");
33
50
  const apiRoot = String(args["api-root"] ?? env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api");
34
51
  if (!apiKey) {
35
52
  if (apiRoot.replace(/\/+$/, "") !== "https://gscdump.com/api") throw new Error("Browser login uses gscdump.com. Supply --api-key for a custom API root.");
36
- const sessionId = await loginWithCloudSession({
53
+ const sessionId = await loginWithHostedSession({
37
54
  request: fetch,
38
55
  now: Date.now,
39
56
  wait: setTimeout,
40
57
  authorize: async (url) => {
41
- logger.info(`Open this URL to connect cloud access:\n${url}`);
58
+ logger.info(`Open this URL to link the CLI to gscdump.com:\n${url}`);
42
59
  if (args.browser !== false) await open(url).catch((error) => logger.warn(`Browser could not open: ${error.message}. Open the URL above.`));
43
60
  }
44
61
  });
45
62
  const state = parseAuthentication({
46
- _tag: "Cloud",
63
+ _tag: "Hosted",
47
64
  apiRoot,
48
65
  sessionId
49
66
  });
50
- if (state._tag !== "Cloud") throw new Error("Cloud login did not return a CLI session.");
51
- const account = await getCloudAccount(state);
67
+ if (state._tag !== "Hosted") throw new Error("Hosted login did not return a CLI session.");
68
+ const account = await getHostedAccount(state);
52
69
  await saveAuthentication(state);
53
- logger.success(`Cloud authentication saved for ${account.user.email}`);
54
- logger.info("Google and Bing commands use connections saved on gscdump.com.");
70
+ logger.success(`Hosted mode saved for ${account.user.email}`);
71
+ logger.info(HOSTED_MODE_NOTE);
55
72
  return;
56
73
  }
57
74
  const state = parseAuthentication({
58
- _tag: "Cloud",
75
+ _tag: "Hosted",
59
76
  apiKey,
60
77
  apiRoot
61
78
  });
62
- if (state._tag !== "Cloud") throw new Error("Cloud login requires a gscdump user API key.");
63
- const account = await getCloudAccount(state);
79
+ if (state._tag !== "Hosted") throw new Error("Hosted login needs a gscdump user API key.");
80
+ const account = await getHostedAccount(state);
64
81
  await saveAuthentication(state);
65
- logger.success(`Cloud authentication saved for ${account.user.email}`);
66
- logger.info("Google and Bing commands use connections saved on gscdump.com.");
82
+ logger.success(`Hosted mode saved for ${account.user.email}`);
83
+ logger.info(HOSTED_MODE_NOTE);
67
84
  }
68
85
  async function resolveLiveAuthState() {
69
86
  const tokens = await loadTokens();
@@ -78,7 +95,7 @@ async function resolveLiveAuthState() {
78
95
  const tokenInfo = verified?.kind === "valid" ? verified.info : null;
79
96
  const failure = current?.kind === "failed" ? current.detail : verified?.kind === "invalid" ? verified.detail : null;
80
97
  const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
81
- const missing = missingRequiredScopes(scopes, byok ? void 0 : tokens?.provider);
98
+ const missing = missingRequiredScopes(scopes);
82
99
  return {
83
100
  byok,
84
101
  tokens: tokens && !byok ? await loadTokens() : tokens,
@@ -93,86 +110,54 @@ async function runStatus(args) {
93
110
  const { json } = applyOutputMode(args);
94
111
  applyAuthMode(args);
95
112
  const authentication = await resolveAuthentication();
96
- if (authentication._tag === "Cloud") {
97
- const capabilities = {
98
- google: [
99
- "sites",
100
- "query",
101
- "sync",
102
- "inspect",
103
- "sitemaps",
104
- "analyze",
105
- "report"
106
- ],
107
- bing: [
108
- "sites",
109
- "dump",
110
- "inspect",
111
- "login",
112
- "status",
113
- "verify"
114
- ],
115
- cloud: [
116
- "sitemaps current",
117
- "sitemaps history",
118
- "sitemaps membership",
119
- "sitemaps lastmod",
120
- "sitemaps export"
121
- ],
122
- local: ["indexing", "sites verification"]
123
- };
124
- const account = await getCloudAccount(authentication).then((value) => ({
113
+ if (authentication._tag === "Hosted") {
114
+ const account = await getHostedAccount(authentication).then((value) => ({
125
115
  _tag: "Ok",
126
116
  value
127
117
  }), (error) => ({
128
118
  _tag: "Err",
129
- detail: error instanceof Error ? error.message : "Hosted authentication failed."
119
+ detail: error instanceof Error ? error.message : "Hosted credentials failed."
130
120
  }));
131
121
  if (account._tag === "Err") {
132
122
  if (json) console.log(JSON.stringify({
133
123
  authenticated: false,
134
- mode: "cloud",
124
+ mode: "hosted",
135
125
  apiRoot: authentication.apiRoot,
136
126
  error: account.detail
137
127
  }, null, 2));
138
128
  else {
139
- logger.warn(`Cloud status check failed: ${account.detail}`);
129
+ logger.warn(`Hosted status check failed: ${account.detail}`);
140
130
  logger.info(`API: ${authentication.apiRoot}`);
141
131
  logger.info("Fix connectivity or the API key, then run `gscdump auth status` again.");
142
132
  }
143
133
  return;
144
134
  }
145
- const hosted = await getCloudSites(authentication).then((sites) => ({
146
- _tag: "Ok",
147
- sites: sites.filter((site) => site.registered)
148
- }), (error) => ({
149
- _tag: "Err",
150
- detail: error instanceof Error ? error.message : "Hosted Site list failed."
151
- }));
135
+ const sites = account.value.sites;
152
136
  if (json) console.log(JSON.stringify({
153
137
  authenticated: true,
154
- mode: "cloud",
138
+ mode: "hosted",
155
139
  account: account.value.user.email,
156
140
  apiRoot: authentication.apiRoot,
157
- sites: account.value.sites,
158
- ...hosted._tag === "Ok" ? { hostedSync: hosted.sites.map((site) => ({
141
+ sites: sites.map((site) => ({
142
+ siteId: site.siteId,
143
+ siteUrl: site.siteUrl
144
+ })),
145
+ hostedSync: sites.map((site) => ({
159
146
  siteUrl: site.siteUrl,
160
147
  syncStatus: site.syncStatus ?? null,
161
148
  syncProgress: site.syncProgress ?? null,
162
149
  oldestDateSynced: site.oldestDateSynced ?? null,
163
150
  newestDateSynced: site.newestDateSynced ?? null
164
- })) } : { hostedSyncError: hosted.detail },
165
- capabilities
151
+ })),
152
+ commands: HOSTED_COMMANDS
166
153
  }, null, 2));
167
154
  else {
168
- logger.success(`Authenticated with cloud: ${account.value.user.email}`);
155
+ logger.success(`Hosted mode: ${account.value.user.email}`);
169
156
  console.log(` API: ${authentication.apiRoot}`);
170
- console.log(` Sites: ${account.value.sites.length}`);
171
- if (hosted._tag === "Ok") for (const site of hosted.sites) console.log(` ${site.siteUrl} ${formatHostedSync(site)}`);
172
- else console.log(` Hosted sync status is not available: ${hosted.detail}`);
173
- console.log(" Google and Bing use connections saved on gscdump.com.");
174
- console.log(` Cloud commands: ${capabilities.cloud.join(", ")}`);
175
- console.log(" Google indexing and Site Verification require --mode local.");
157
+ console.log(` Sites: ${sites.length}`);
158
+ for (const site of sites) console.log(` ${site.siteUrl} ${formatHostedSync(site)}`);
159
+ console.log(` ${HOSTED_MODE_NOTE}`);
160
+ console.log(` Hosted commands: ${HOSTED_COMMANDS.join(", ")}`);
176
161
  }
177
162
  return;
178
163
  }
@@ -193,8 +178,8 @@ async function runStatus(args) {
193
178
  googleAuthenticated,
194
179
  googleError: failure,
195
180
  bing,
196
- source: byok ? "byok" : tokens ? "saved-tokens" : null,
197
- byokKind,
181
+ source: byok ? "env" : tokens ? "saved-tokens" : null,
182
+ envCredential: byokKind,
198
183
  scopes,
199
184
  tokenAccount: tokenInfo?.email ?? null,
200
185
  tokens: tokens ? {
@@ -219,8 +204,8 @@ async function runStatus(args) {
219
204
  }
220
205
  };
221
206
  if (byok) {
222
- if (googleAuthenticated) logger.success(`Authenticated via BYOK (${byokKind})`);
223
- else logger.warn(`BYOK credentials (${byokKind}) failed verification: ${failure}`);
207
+ if (googleAuthenticated) logger.success(`Authenticated with environment credentials (${byokKind})`);
208
+ else logger.warn(`Environment credentials (${byokKind}) failed verification: ${failure}`);
224
209
  if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
225
210
  reportScopes();
226
211
  return;
@@ -230,7 +215,7 @@ async function runStatus(args) {
230
215
  logger.info("Google credentials are missing. Run `gscdump auth login --mode local` to connect Google.");
231
216
  return;
232
217
  }
233
- logger.warn(GOOGLE_NOT_CONNECTED);
218
+ logger.warn(ACCESS_NOT_SET_UP);
234
219
  return;
235
220
  }
236
221
  const hasAccess = !!tokens.access_token;
@@ -262,7 +247,7 @@ const statusCommand = defineCommand({
262
247
  const refreshCommand = defineCommand({
263
248
  meta: {
264
249
  name: "refresh",
265
- description: "Force-refresh saved OAuth tokens (no-op for BYOK)"
250
+ description: "Force-refresh saved OAuth tokens (no-op for environment credentials)"
266
251
  },
267
252
  args: {
268
253
  ...OUTPUT_ARGS,
@@ -272,7 +257,7 @@ const refreshCommand = defineCommand({
272
257
  applyOutputMode(args);
273
258
  await requireLocalAuth(args);
274
259
  if (resolveBYOK()) {
275
- logger.info("BYOK detected; refresh handled per-call by the SDK");
260
+ logger.info("Environment credentials found. The SDK refreshes them for each call.");
276
261
  return;
277
262
  }
278
263
  const tokens = await loadTokens();
@@ -298,11 +283,11 @@ const loginCommand = defineCommand({
298
283
  "mode": MODE_ARG,
299
284
  "api-key": {
300
285
  type: "string",
301
- description: "gscdump user API key; defaults to GSCDUMP_API_KEY"
286
+ description: "gscdump user API key for Hosted mode; defaults to GSCDUMP_API_KEY"
302
287
  },
303
288
  "api-root": {
304
289
  type: "string",
305
- description: "Cloud API root; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
290
+ description: "Hosted API root; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
306
291
  },
307
292
  "force": {
308
293
  type: "boolean",
@@ -324,13 +309,13 @@ const loginCommand = defineCommand({
324
309
  applyOutputMode(args);
325
310
  const runtime = useCliRuntime();
326
311
  const requestedMode = parseAuthMode(args.mode) ?? runtime.authModeOverride ?? parseAuthMode(runtime.environment.GSCDUMP_AUTH_MODE);
327
- if (requestedMode === "cloud" || !requestedMode && (args["api-key"] || (await resolveAuthentication())._tag === "Cloud")) {
328
- await loginCloud(args);
312
+ if (requestedMode === "hosted" || !requestedMode && (args["api-key"] || (await resolveAuthentication())._tag === "Hosted")) {
313
+ await loginHosted(args);
329
314
  return;
330
315
  }
331
316
  if (resolveBYOK() && !args.force) {
332
317
  await saveAuthentication({ _tag: "Local" });
333
- logger.info("BYOK env vars detected, no login needed (--force to override)");
318
+ logger.info("Google credentials found in the environment. No login is needed. Pass --force to log in again.");
334
319
  return;
335
320
  }
336
321
  if (args["service-account"]) {
@@ -385,9 +370,9 @@ const logoutCommand = defineCommand({
385
370
  args: { ...OUTPUT_ARGS },
386
371
  async run({ args }) {
387
372
  applyOutputMode(args);
388
- await resolveAuthentication().then((authentication) => authentication._tag === "Cloud" ? revokeCloudSession(authentication) : void 0).catch((error) => {
373
+ await resolveAuthentication().then((authentication) => authentication._tag === "Hosted" ? revokeHostedSession(authentication) : void 0).catch((error) => {
389
374
  const message = error instanceof Error ? error.message : String(error);
390
- logger.warn(`Cloud session revocation failed (${message}). Local credentials are still cleared.`);
375
+ logger.warn(`Hosted session revocation failed (${message}). Saved credentials are still cleared.`);
391
376
  });
392
377
  await clearTokens();
393
378
  await clearBingCredentials();
@@ -446,4 +431,4 @@ const authCommand = defineCommand({
446
431
  await runStatus(args);
447
432
  }
448
433
  });
449
- export { authCommand, loginCloud, loginCommand, logoutCommand, statusCommand };
434
+ export { HOSTED_COMMANDS, authCommand, loginCommand, loginHosted, logoutCommand, statusCommand };
@@ -56,8 +56,8 @@ const bingCommand = defineCommand({
56
56
  const { json } = applyOutputMode(args);
57
57
  const env = useCliRuntime().environment;
58
58
  const authentication = await resolveAuthentication();
59
- if (authentication._tag === "Cloud") {
60
- if (args.oauth || args["api-key"] || args["client-id"] || args["client-secret"] || args["redirect-uri"]) throw new Error("Local Bing credentials require --mode local. Hosted login uses `gscdump auth login --mode cloud`.");
59
+ if (authentication._tag === "Hosted") {
60
+ if (args.oauth || args["api-key"] || args["client-id"] || args["client-secret"] || args["redirect-uri"]) throw new Error("Local Bing credentials need Local mode. Pass --mode local. Hosted mode uses the Bing connection saved on gscdump.com.");
61
61
  if (!args.site) throw new Error("Choose --site SITE_ID from `gscdump bing sites` to connect Bing.");
62
62
  const [site] = await resolveHostedBingSites(authentication, {
63
63
  site: args.site,
@@ -128,7 +128,7 @@ const bingCommand = defineCommand({
128
128
  args: OUTPUT_ARGS,
129
129
  async run({ args }) {
130
130
  const { json } = applyOutputMode(args);
131
- if ((await resolveAuthentication())._tag === "Cloud") throw new Error("Hosted authentication is shared with Google. Use `gscdump auth logout` to remove it.");
131
+ if ((await resolveAuthentication())._tag === "Hosted") throw new Error("Hosted authentication is shared with Google. Use `gscdump auth logout` to remove it.");
132
132
  await clearBingCredentials();
133
133
  const env = useCliRuntime().environment;
134
134
  const environmentCredentials = Boolean(env.BING_API_KEY?.trim() || env.BING_ACCESS_TOKEN?.trim());
@@ -152,14 +152,14 @@ const bingCommand = defineCommand({
152
152
  async run({ args }) {
153
153
  const { json } = applyOutputMode(args);
154
154
  const authentication = await resolveAuthentication();
155
- if (authentication._tag === "Cloud") {
155
+ if (authentication._tag === "Hosted") {
156
156
  const sites = args.site ? await resolveHostedBingSites(authentication, {
157
157
  site: args.site,
158
158
  requireConnected: false
159
159
  }) : await listHostedBingSites(authentication);
160
160
  console.log(JSON.stringify({
161
161
  searchEngine: "bing",
162
- mode: "cloud",
162
+ mode: "hosted",
163
163
  sites
164
164
  }, null, 2));
165
165
  return;
@@ -183,7 +183,7 @@ const bingCommand = defineCommand({
183
183
  async run({ args }) {
184
184
  applyOutputMode(args);
185
185
  const authentication = await resolveAuthentication();
186
- if (authentication._tag === "Cloud") {
186
+ if (authentication._tag === "Hosted") {
187
187
  console.log(JSON.stringify({
188
188
  searchEngine: "bing",
189
189
  sites: await listHostedBingSites(authentication)
@@ -223,7 +223,7 @@ const bingCommand = defineCommand({
223
223
  },
224
224
  "datasets": {
225
225
  type: "string",
226
- description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all. Crawl issues have no dates, so --start, --end, and cloud mode leave them out`
226
+ description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all. Crawl issues have no dates, so --start, --end, and Hosted mode leave them out`
227
227
  },
228
228
  "start": {
229
229
  type: "string",
@@ -239,7 +239,7 @@ const bingCommand = defineCommand({
239
239
  const { json } = applyOutputMode(args);
240
240
  const options = parseBingDumpOptions(args);
241
241
  const authentication = await resolveAuthentication();
242
- if (authentication._tag === "Cloud") {
242
+ if (authentication._tag === "Hosted") {
243
243
  if (options.datasetsExplicit && options.datasets.includes("crawl-issues")) throw new Error("Bing crawl issues require local authentication. Use --mode local.");
244
244
  if (options.start && options.end && Date.parse(options.end) - Date.parse(options.start) > 316224e5) throw new Error("Hosted Bing exports support at most 366 days. Use a shorter --start and --end range.");
245
245
  const sites = await resolveHostedBingSites(authentication, {
@@ -297,7 +297,7 @@ const bingCommand = defineCommand({
297
297
  const url = URL.parse(args.url);
298
298
  if (!url || !["http:", "https:"].includes(url.protocol)) throw new Error("Use a full HTTP or HTTPS page URL.");
299
299
  const authentication = await resolveAuthentication();
300
- if (authentication._tag === "Cloud") {
300
+ if (authentication._tag === "Hosted") {
301
301
  const [site] = await resolveHostedBingSites(authentication, { site: args.site });
302
302
  console.log(JSON.stringify(await inspectHostedBingUrl(authentication, site.siteId, args.url), null, 2));
303
303
  return;
@@ -323,7 +323,7 @@ const bingCommand = defineCommand({
323
323
  async run({ args }) {
324
324
  applyOutputMode(args);
325
325
  const authentication = await resolveAuthentication();
326
- if (authentication._tag !== "Cloud") throw new Error("Hosted Bing connection verification requires --mode cloud.");
326
+ if (authentication._tag !== "Hosted") throw new Error("Hosted Bing connection verification needs Hosted mode. Pass --mode hosted.");
327
327
  const [site] = await resolveHostedBingSites(authentication, {
328
328
  site: args.site,
329
329
  requireConnected: false
@@ -1,10 +1,10 @@
1
1
  import { doctorCommandMeta } from "../command-meta.mjs";
2
- import { getCloudAccount, resolveAuthentication } from "../auth-state.mjs";
2
+ import { getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
3
3
  import { loadConfig } from "../config.mjs";
4
4
  import { resolveCliEnvironment } from "../environment.mjs";
5
5
  import { parseEnvFile } from "../env-file.mjs";
6
6
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
- import { getAuth, loadTokens, resolveAuth, resolveBYOK } from "../auth.mjs";
7
+ import { getAuth, isStaleServiceAccountPointer, loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
8
8
  import { createLocalStore } from "../local-store.mjs";
9
9
  import { createCommandContext, formatSiteResolution } from "../context.mjs";
10
10
  import { missingRequiredScopes } from "../auth-scopes.mjs";
@@ -76,17 +76,62 @@ function describeAuthSource(envKeys, byok) {
76
76
  const isAccessToken = typeof byok === "string";
77
77
  const driver = isAccessToken ? "GSC_ACCESS_TOKEN" : "GSC_REFRESH_TOKEN";
78
78
  const source = (isAccessToken ? ["GSC_ACCESS_TOKEN", "GOOGLE_ACCESS_TOKEN"] : ["GSC_REFRESH_TOKEN", "GOOGLE_REFRESH_TOKEN"]).some((k) => envKeys.has(k)) ? ".env" : "shell env";
79
- return `BYOK ${isAccessToken ? "(access-token)" : "(refresh-token)"} from ${source} via ${driver}`;
79
+ return `environment credentials ${isAccessToken ? "(access-token)" : "(refresh-token)"} from ${source} via ${driver}`;
80
80
  }
81
81
  async function checkAuth(envKeys) {
82
82
  const checks = [];
83
+ const serviceAccount = await resolveServiceAccount().catch((error) => {
84
+ if (isStaleServiceAccountPointer(error)) {
85
+ checks.push({
86
+ name: "auth.service_account",
87
+ status: "warn",
88
+ detail: `ignored: ${error instanceof Error ? error.message : String(error)}`
89
+ });
90
+ return null;
91
+ }
92
+ return error instanceof Error ? error : new Error(String(error));
93
+ });
94
+ if (serviceAccount instanceof Error) {
95
+ checks.push({
96
+ name: "auth",
97
+ status: "fail",
98
+ detail: `service account: ${serviceAccount.message}`
99
+ });
100
+ return {
101
+ checks,
102
+ liveToken: null
103
+ };
104
+ }
105
+ if (serviceAccount) {
106
+ const token = await serviceAccount.getAccessToken().then((result) => result.token ?? null, (error) => error instanceof Error ? error : new Error(String(error)));
107
+ if (!token || token instanceof Error) {
108
+ checks.push({
109
+ name: "auth",
110
+ status: "fail",
111
+ detail: `service account ${serviceAccount.email}: ${token instanceof Error ? redactTokens(token.message) : "no access token"}`
112
+ });
113
+ return {
114
+ checks,
115
+ liveToken: null
116
+ };
117
+ }
118
+ checks.push({
119
+ name: "auth",
120
+ status: "pass",
121
+ detail: `service account ${serviceAccount.email}`
122
+ });
123
+ return {
124
+ checks,
125
+ liveToken: token
126
+ };
127
+ }
83
128
  const byok = resolveBYOK();
84
129
  const tokens = await loadTokens();
85
130
  if (!byok && !tokens) {
86
131
  checks.push({
87
132
  name: "auth",
88
133
  status: "fail",
89
- detail: "no BYOK env vars and no saved tokens; run `gscdump init`"
134
+ detail: "no Google credentials: no service account, no environment tokens, no saved tokens. Run `gscdump init`, or `gscdump auth login --mode hosted` for Hosted mode"
90
135
  });
91
136
  return {
92
137
  checks,
@@ -140,7 +185,7 @@ async function checkAuth(envKeys) {
140
185
  detail: info.email
141
186
  });
142
187
  const scopes = info.scope ? info.scope.split(/\s+/) : [];
143
- const missing = missingRequiredScopes(scopes, byok ? void 0 : tokens?.provider);
188
+ const missing = missingRequiredScopes(scopes);
144
189
  if (missing.length > 0) checks.push({
145
190
  name: "auth.scopes",
146
191
  status: "warn",
@@ -324,8 +369,8 @@ function describeGscSites(sites, defaultSite) {
324
369
  }
325
370
  return checks;
326
371
  }
327
- async function checkCloudConnection(authentication) {
328
- const account = await getCloudAccount(authentication).then((value) => ({
372
+ async function checkHostedConnection(authentication) {
373
+ const account = await getHostedAccount(authentication).then((value) => ({
329
374
  _tag: "Ok",
330
375
  value
331
376
  }), (error) => ({
@@ -341,16 +386,21 @@ async function checkCloudConnection(authentication) {
341
386
  status: "warn",
342
387
  detail: "skipped (auth failed)"
343
388
  }];
344
- const sitesChecks = await createCommandContext({ needsAuth: true }).then(async (context) => describeGscSites(await context.loadSites(), context.config.defaultSite)).catch((error) => [{
345
- name: "gsc.sites",
346
- status: "fail",
347
- detail: error instanceof Error ? error.message : "Hosted Sites request failed."
348
- }]);
389
+ const count = account.value.sites.length;
390
+ const sitesChecks = [count > 0 ? {
391
+ name: "hosted.sites",
392
+ status: "pass",
393
+ detail: `${count} hosted Site(s)`
394
+ } : {
395
+ name: "hosted.sites",
396
+ status: "warn",
397
+ detail: "no hosted Sites. Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites"
398
+ }];
349
399
  return [
350
400
  {
351
401
  name: "auth",
352
402
  status: "pass",
353
- detail: `cloud via ${authentication.apiRoot}`
403
+ detail: `Hosted mode via ${authentication.apiRoot}`
354
404
  },
355
405
  {
356
406
  name: "auth.account",
@@ -391,7 +441,7 @@ const doctorCommand = defineCommand({
391
441
  const authentication = await resolveAuthentication();
392
442
  const envResult = await checkEnv();
393
443
  const [connectionChecks, dataDirChecks, watermarkChecks] = await Promise.all([
394
- authentication._tag === "Cloud" ? checkCloudConnection(authentication) : checkLocalConnection(envResult.envKeys),
444
+ authentication._tag === "Hosted" ? checkHostedConnection(authentication) : checkLocalConnection(envResult.envKeys),
395
445
  checkDataDir(dataDir),
396
446
  checkStoreWatermarks(dataDir)
397
447
  ]);
@@ -4,8 +4,8 @@ import { saveAuthentication } from "../auth-state.mjs";
4
4
  import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
5
5
  import { applyCliEnvironment } from "../environment.mjs";
6
6
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
- import { authenticate, getAuth, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
8
- import { loginCloud } from "./auth.mjs";
7
+ import { ACCESS_NOT_SET_UP, authenticate, getAuth, loadTokens, resolveBYOK, resolveServiceAccount, saveTokens } from "../auth.mjs";
8
+ import { loginHosted } from "./auth.mjs";
9
9
  import process from "node:process";
10
10
  import { defineCommand } from "citty";
11
11
  import fs from "node:fs/promises";
@@ -61,15 +61,15 @@ const initCommand = defineCommand({
61
61
  },
62
62
  "mode": {
63
63
  type: "string",
64
- description: "Authentication mode to save: cloud or local"
64
+ description: "Access mode to save: local or hosted"
65
65
  },
66
66
  "api-key": {
67
67
  type: "string",
68
- description: "gscdump user API key for --mode cloud; defaults to GSCDUMP_API_KEY"
68
+ description: "gscdump user API key for --mode hosted; defaults to GSCDUMP_API_KEY"
69
69
  },
70
70
  "api-root": {
71
71
  type: "string",
72
- description: "Cloud API root for --mode cloud; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
72
+ description: "Hosted API root for --mode hosted; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
73
73
  },
74
74
  ...OUTPUT_ARGS
75
75
  },
@@ -77,9 +77,9 @@ const initCommand = defineCommand({
77
77
  applyOutputMode(args);
78
78
  const config = await loadConfig();
79
79
  const mode = useCliRuntime().authModeOverride;
80
- if (mode === "cloud") {
81
- await loginCloud(args);
82
- printNextSteps();
80
+ if (mode === "hosted") {
81
+ await loginHosted(args);
82
+ printHostedNextSteps();
83
83
  return;
84
84
  }
85
85
  if (config.clientId && config.clientSecret && !args.force) {
@@ -104,11 +104,23 @@ const initCommand = defineCommand({
104
104
  ...dataDir ? { dataDir } : {}
105
105
  });
106
106
  await saveAuthentication({ _tag: "Local" });
107
- logger.success(`BYOK detected (${typeof byok === "string" ? "access-token" : "refresh-token"}). Auth setup skipped.`);
107
+ logger.success(`Google credentials found in the environment (${typeof byok === "string" ? "access-token" : "refresh-token"}). Login skipped.`);
108
108
  logger.success("Setup complete.");
109
109
  printNextSteps();
110
110
  return;
111
111
  }
112
+ const serviceAccount = await resolveServiceAccount().catch(() => null);
113
+ if (serviceAccount) {
114
+ const dataDir = args.store ? config.dataDir ?? defaultDataDir() : config.dataDir;
115
+ await saveConfig({
116
+ ...config,
117
+ ...dataDir ? { dataDir } : {}
118
+ });
119
+ await saveAuthentication({ _tag: "Local" });
120
+ logger.success(`Service account found (${serviceAccount.email ?? "key file"}). Login skipped.`);
121
+ printNextSteps();
122
+ return;
123
+ }
112
124
  const envFile = await loadEnvFile();
113
125
  const envCid = envFile?.GSC_CLIENT_ID ?? envFile?.GOOGLE_CLIENT_ID;
114
126
  const envSec = envFile?.GSC_CLIENT_SECRET ?? envFile?.GOOGLE_CLIENT_SECRET;
@@ -145,11 +157,7 @@ const initCommand = defineCommand({
145
157
  }
146
158
  if (!canPrompt()) {
147
159
  if (!await loadTokens()) {
148
- logger.error([
149
- "No Google credentials found. Init cannot prompt without a terminal.",
150
- "Run `gscdump auth login` in a terminal, or set GSC_CLIENT_ID, GSC_CLIENT_SECRET and GSC_REFRESH_TOKEN.",
151
- "To use gscdump.com, run `gscdump auth login --mode cloud`."
152
- ].join("\n"));
160
+ logger.error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
153
161
  process.exit(1);
154
162
  }
155
163
  await saveConfig({
@@ -188,6 +196,14 @@ function printNextSteps(sites = []) {
188
196
  line(`gscdump sync --status --site ${site}`, "see what the Store holds");
189
197
  console.log();
190
198
  }
199
+ function printHostedNextSteps() {
200
+ console.log();
201
+ console.log(" Next:");
202
+ const line = (command, note) => console.log(` ${command.padEnd(44)} # ${note}`);
203
+ line("gscdump sites", "list your hosted Sites and their sync state");
204
+ line("gscdump query --site <site> -d query", "read the hosted record");
205
+ console.log();
206
+ }
191
207
  async function runSmokeTest(oauth) {
192
208
  const sites = await googleSearchConsole(oauth).sites().catch((e) => e);
193
209
  if (sites instanceof Error) {
@@ -241,4 +257,4 @@ async function maybeWriteEnvFile(clientId, clientSecret) {
241
257
  await fs.writeFile(envPath, content, { mode: 384 });
242
258
  logger.success(`Wrote ${displayPath(envPath)}`);
243
259
  }
244
- export { initCommand, printNextSteps, runSmokeTest };
260
+ export { initCommand, printHostedNextSteps, printNextSteps, runSmokeTest };