@gscdump/cli 0.37.3 → 0.37.4
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/bin/gscdump.mjs +3 -0
- package/dist/_chunks/analysis-local.mjs +88 -0
- package/dist/_chunks/analyze.mjs +332 -0
- package/dist/_chunks/auth.mjs +476 -0
- package/dist/_chunks/auth2.mjs +293 -0
- package/dist/_chunks/config.mjs +93 -0
- package/dist/_chunks/config2.mjs +232 -0
- package/dist/_chunks/context.mjs +69 -0
- package/dist/_chunks/doctor.mjs +400 -0
- package/dist/_chunks/dump.mjs +193 -0
- package/dist/_chunks/entities.mjs +414 -0
- package/dist/_chunks/env-file.mjs +53 -0
- package/dist/_chunks/environment.mjs +30 -0
- package/dist/_chunks/error-handler.mjs +86 -0
- package/dist/_chunks/indexing.mjs +314 -0
- package/dist/_chunks/init.mjs +190 -0
- package/dist/_chunks/inspect.mjs +203 -0
- package/dist/_chunks/mcp.mjs +867 -0
- package/dist/_chunks/native-duckdb.mjs +46 -0
- package/dist/_chunks/profile.mjs +290 -0
- package/dist/_chunks/query.mjs +537 -0
- package/dist/_chunks/report.mjs +243 -0
- package/dist/_chunks/sitemaps.mjs +274 -0
- package/dist/_chunks/sites.mjs +500 -0
- package/dist/_chunks/store.mjs +652 -0
- package/dist/_chunks/sync.mjs +489 -0
- package/dist/_chunks/utils.mjs +191 -0
- package/dist/cli.d.mts +28 -0
- package/dist/cli.mjs +104 -0
- package/package.json +15 -7
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -7330
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { getConfigDir, loadConfig } from "./config.mjs";
|
|
2
|
+
import { pickCliEnvironmentValue, resolveCliEnvironment } from "./environment.mjs";
|
|
3
|
+
import { displayPath, logger } from "./utils.mjs";
|
|
4
|
+
import { getAppliedEnvKeys, getLoadedEnvPath } from "./env-file.mjs";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { isCancel, text } from "@clack/prompts";
|
|
9
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
10
|
+
import { createAuth } from "gscdump/api";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { JWT, OAuth2Client } from "google-auth-library";
|
|
13
|
+
import { ofetch } from "ofetch";
|
|
14
|
+
function authErrorToException(error) {
|
|
15
|
+
const exception = new Error(error.message);
|
|
16
|
+
if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
|
|
17
|
+
exception.authError = error;
|
|
18
|
+
return exception;
|
|
19
|
+
}
|
|
20
|
+
const SCOPES = [
|
|
21
|
+
"https://www.googleapis.com/auth/webmasters",
|
|
22
|
+
"https://www.googleapis.com/auth/indexing",
|
|
23
|
+
"https://www.googleapis.com/auth/siteverification"
|
|
24
|
+
];
|
|
25
|
+
async function loadServiceAccountResult(jsonPath) {
|
|
26
|
+
const raw = await fs.readFile(jsonPath, "utf-8");
|
|
27
|
+
const key = JSON.parse(raw);
|
|
28
|
+
if (key.type !== "service_account") return err({
|
|
29
|
+
kind: "not-service-account",
|
|
30
|
+
path: jsonPath,
|
|
31
|
+
accountType: key.type,
|
|
32
|
+
message: `${jsonPath} is not a service-account key (type=${key.type})`
|
|
33
|
+
});
|
|
34
|
+
return ok(new JWT({
|
|
35
|
+
email: key.client_email,
|
|
36
|
+
key: key.private_key,
|
|
37
|
+
scopes: SCOPES
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async function loadServiceAccount(jsonPath) {
|
|
41
|
+
return unwrapResult(await loadServiceAccountResult(jsonPath), authErrorToException);
|
|
42
|
+
}
|
|
43
|
+
async function resolveServiceAccount(opts = {}) {
|
|
44
|
+
let p = opts.path || resolveCliEnvironment().serviceAccountPath;
|
|
45
|
+
if (!p) p = (await loadConfig().catch(() => null))?.serviceAccountPath;
|
|
46
|
+
if (!p) return null;
|
|
47
|
+
return loadServiceAccount(p);
|
|
48
|
+
}
|
|
49
|
+
async function authenticateDeviceCode(credentials) {
|
|
50
|
+
return unwrapResult(await authenticateDeviceCodeResult(credentials), authErrorToException);
|
|
51
|
+
}
|
|
52
|
+
async function authenticateDeviceCodeResult(credentials) {
|
|
53
|
+
const init = await ofetch("https://oauth2.googleapis.com/device/code", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: new URLSearchParams({
|
|
56
|
+
client_id: credentials.clientId,
|
|
57
|
+
scope: SCOPES.join(" ")
|
|
58
|
+
})
|
|
59
|
+
}).then(ok).catch((e) => err({
|
|
60
|
+
kind: "device-code-request-failed",
|
|
61
|
+
message: `Device-code request failed: ${e.message}`,
|
|
62
|
+
cause: e
|
|
63
|
+
}));
|
|
64
|
+
if (!init.ok) return init;
|
|
65
|
+
return pollDeviceCode(credentials, init.value);
|
|
66
|
+
}
|
|
67
|
+
async function pollDeviceCode(credentials, init) {
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(` \x1B[1mDevice-code OAuth\x1B[0m`);
|
|
70
|
+
console.log(` 1. On any device, open: \x1B[36m${init.verification_url}\x1B[0m`);
|
|
71
|
+
console.log(` 2. Enter this code: \x1B[1m${init.user_code}\x1B[0m`);
|
|
72
|
+
console.log(` 3. Approve the requested scopes`);
|
|
73
|
+
console.log();
|
|
74
|
+
logger.info(`Polling for completion (expires in ${Math.floor(init.expires_in / 60)}m)...`);
|
|
75
|
+
const intervalMs = init.interval * 1e3;
|
|
76
|
+
const deadline = Date.now() + init.expires_in * 1e3;
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
79
|
+
const res = await ofetch("https://oauth2.googleapis.com/token", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: new URLSearchParams({
|
|
82
|
+
client_id: credentials.clientId,
|
|
83
|
+
client_secret: credentials.clientSecret,
|
|
84
|
+
device_code: init.device_code,
|
|
85
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
86
|
+
})
|
|
87
|
+
}).catch((e) => e?.data ?? { error: "request_failed" });
|
|
88
|
+
if (res.access_token) return ok({
|
|
89
|
+
access_token: res.access_token,
|
|
90
|
+
refresh_token: res.refresh_token,
|
|
91
|
+
expiry_date: res.expires_in ? Date.now() + res.expires_in * 1e3 : void 0
|
|
92
|
+
});
|
|
93
|
+
if (res.error === "authorization_pending" || res.error === "slow_down") continue;
|
|
94
|
+
if (res.error === "access_denied") return err({
|
|
95
|
+
kind: "device-code-denied",
|
|
96
|
+
message: "User denied authorization."
|
|
97
|
+
});
|
|
98
|
+
if (res.error === "expired_token") return err({
|
|
99
|
+
kind: "device-code-expired",
|
|
100
|
+
message: "Device code expired. Re-run `gscdump auth login --no-browser`."
|
|
101
|
+
});
|
|
102
|
+
if (res.error) return err({
|
|
103
|
+
kind: "device-code-failed",
|
|
104
|
+
reason: res.error,
|
|
105
|
+
message: `Device-code poll failed: ${res.error_description || res.error}`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return err({
|
|
109
|
+
kind: "device-code-timed-out",
|
|
110
|
+
message: "Device-code flow timed out."
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function resolveBYOK(opts = {}) {
|
|
114
|
+
const env = resolveCliEnvironment();
|
|
115
|
+
const accessToken = opts.accessToken || env.accessToken;
|
|
116
|
+
const clientId = opts.clientId || env.clientId;
|
|
117
|
+
const clientSecret = opts.clientSecret || env.clientSecret;
|
|
118
|
+
const refreshToken = opts.refreshToken || env.refreshToken;
|
|
119
|
+
if (clientId && clientSecret && refreshToken) return createAuth({
|
|
120
|
+
clientId,
|
|
121
|
+
clientSecret,
|
|
122
|
+
refreshToken
|
|
123
|
+
});
|
|
124
|
+
if (accessToken) return accessToken;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const REDIRECT_URI_RE = /redirect_uri=[^&]+/;
|
|
128
|
+
function getTokensPath() {
|
|
129
|
+
return path.join(getConfigDir(), "tokens.json");
|
|
130
|
+
}
|
|
131
|
+
async function loadTokens() {
|
|
132
|
+
return fs.readFile(getTokensPath(), "utf-8").then((data) => JSON.parse(data)).catch(() => null);
|
|
133
|
+
}
|
|
134
|
+
async function saveTokens(tokens) {
|
|
135
|
+
await fs.mkdir(getConfigDir(), {
|
|
136
|
+
recursive: true,
|
|
137
|
+
mode: 448
|
|
138
|
+
});
|
|
139
|
+
await fs.writeFile(getTokensPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
140
|
+
}
|
|
141
|
+
async function clearTokens() {
|
|
142
|
+
await fs.rm(getTokensPath(), { force: true });
|
|
143
|
+
logger.success("Logged out, tokens cleared");
|
|
144
|
+
}
|
|
145
|
+
async function getAuthCredentials(interactive) {
|
|
146
|
+
const env = resolveCliEnvironment();
|
|
147
|
+
const envClientId = env.clientId;
|
|
148
|
+
const envClientSecret = env.clientSecret;
|
|
149
|
+
if (envClientId && envClientSecret) {
|
|
150
|
+
if (interactive) {
|
|
151
|
+
logger.info("Using OAuth client from env");
|
|
152
|
+
console.log(` \x1B[90m${envClientId}\x1B[0m`);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
clientId: envClientId,
|
|
156
|
+
clientSecret: envClientSecret
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const config = await loadConfig();
|
|
160
|
+
if (config.clientId && config.clientSecret) {
|
|
161
|
+
if (interactive) {
|
|
162
|
+
logger.info(`Using OAuth client from ${displayPath(`${getConfigDir()}/config.json`)}`);
|
|
163
|
+
console.log(` \x1B[90m${config.clientId}\x1B[0m`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
clientId: config.clientId,
|
|
167
|
+
clientSecret: config.clientSecret
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!interactive) {
|
|
171
|
+
logger.error("GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET required for non-interactive mode");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(" \x1B[1mOAuth 2.0 Setup Required\x1B[0m");
|
|
176
|
+
console.log(" \x1B[90mThe Google Search Console API requires OAuth 2.0 credentials.\x1B[0m");
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(" \x1B[1mSteps:\x1B[0m");
|
|
179
|
+
console.log(" \x1B[90m1.\x1B[0m Go to \x1B[36mhttps://console.developers.google.com/apis/credentials\x1B[0m");
|
|
180
|
+
console.log(" \x1B[90m2.\x1B[0m Create credentials > OAuth client ID > Desktop application");
|
|
181
|
+
console.log(" \x1B[90m3.\x1B[0m Enable \"Search Console API\" and \"Web Search Indexing API\" for your project");
|
|
182
|
+
console.log(" \x1B[90m4.\x1B[0m Copy the Client ID and Client Secret");
|
|
183
|
+
console.log();
|
|
184
|
+
const clientIdResult = await text({
|
|
185
|
+
message: "Enter your Google OAuth Client ID:",
|
|
186
|
+
placeholder: "your-client-id.googleusercontent.com",
|
|
187
|
+
validate: (v) => v ? void 0 : "Required"
|
|
188
|
+
});
|
|
189
|
+
if (isCancel(clientIdResult)) process.exit(1);
|
|
190
|
+
const clientSecretResult = await text({
|
|
191
|
+
message: "Enter your Google OAuth Client Secret:",
|
|
192
|
+
validate: (v) => v ? void 0 : "Required"
|
|
193
|
+
});
|
|
194
|
+
if (isCancel(clientSecretResult)) process.exit(1);
|
|
195
|
+
console.log();
|
|
196
|
+
logger.info("Tip: Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars to skip prompts");
|
|
197
|
+
return {
|
|
198
|
+
clientId: clientIdResult,
|
|
199
|
+
clientSecret: clientSecretResult
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function getAuthCodeViaLoopback(authUrl) {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
let resolvedRedirectUri = "";
|
|
205
|
+
let timeoutId;
|
|
206
|
+
let server;
|
|
207
|
+
const settle = (fn) => {
|
|
208
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
209
|
+
server.closeAllConnections?.();
|
|
210
|
+
server.close();
|
|
211
|
+
fn();
|
|
212
|
+
};
|
|
213
|
+
server = createServer((req, res) => {
|
|
214
|
+
const url = new URL(req.url || "", `http://127.0.0.1`);
|
|
215
|
+
const code = url.searchParams.get("code");
|
|
216
|
+
const error = url.searchParams.get("error");
|
|
217
|
+
if (error) {
|
|
218
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
219
|
+
res.end(`<html><body><h1>Authorization Failed</h1><p>${error}</p><p>You can close this window.</p></body></html>`);
|
|
220
|
+
settle(() => reject(/* @__PURE__ */ new Error(`OAuth error: ${error}`)));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (code) {
|
|
224
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
225
|
+
res.end(`<html><body><h1>Authorization Successful</h1><p>You can close this window and return to the terminal.</p></body></html>`);
|
|
226
|
+
settle(() => resolve({
|
|
227
|
+
code,
|
|
228
|
+
redirectUri: resolvedRedirectUri
|
|
229
|
+
}));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
233
|
+
res.end(`<html><body><h1>Missing authorization code</h1></body></html>`);
|
|
234
|
+
});
|
|
235
|
+
server.listen(0, "127.0.0.1", () => {
|
|
236
|
+
const addr = server.address();
|
|
237
|
+
if (!addr || typeof addr === "string") {
|
|
238
|
+
settle(() => reject(/* @__PURE__ */ new Error("Failed to start local server")));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
resolvedRedirectUri = `http://127.0.0.1:${addr.port}`;
|
|
242
|
+
const fullAuthUrl = authUrl.replace(REDIRECT_URI_RE, `redirect_uri=${encodeURIComponent(resolvedRedirectUri)}`);
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(" \x1B[1mOpening browser for authorization...\x1B[0m");
|
|
245
|
+
console.log(` \x1B[90mIf browser doesn't open, visit:\x1B[0m`);
|
|
246
|
+
console.log(` \x1B[36m${fullAuthUrl}\x1B[0m`);
|
|
247
|
+
console.log();
|
|
248
|
+
console.log(` \x1B[90mIf Google says "redirect_uri_mismatch", your OAuth client is`);
|
|
249
|
+
console.log(` not a "Desktop application" type. Create a Desktop client at`);
|
|
250
|
+
console.log(` https://console.cloud.google.com/apis/credentials, then run`);
|
|
251
|
+
console.log(` \`gscdump init --force\` with the new ID/secret.\x1B[0m`);
|
|
252
|
+
console.log();
|
|
253
|
+
import("open").then(({ default: open }) => open(fullAuthUrl)).catch(() => {
|
|
254
|
+
logger.warn("Could not open browser automatically");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
server.on("error", (err) => settle(() => reject(err)));
|
|
258
|
+
timeoutId = setTimeout(() => {
|
|
259
|
+
settle(() => reject(/* @__PURE__ */ new Error("Authorization timed out. If Google showed \"redirect_uri_mismatch\", your OAuth client must be type \"Desktop application\" (create one at https://console.cloud.google.com/apis/credentials and run `gscdump init --force`).")));
|
|
260
|
+
}, 300 * 1e3);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async function authenticate(credentials, interactive, opts = {}) {
|
|
264
|
+
const oauth2Client = new OAuth2Client(credentials.clientId, credentials.clientSecret, "http://127.0.0.1");
|
|
265
|
+
const env = resolveCliEnvironment();
|
|
266
|
+
const envAccessToken = !opts.force ? env.accessToken : void 0;
|
|
267
|
+
const envRefreshToken = !opts.force ? env.refreshToken : void 0;
|
|
268
|
+
if (envAccessToken || envRefreshToken) {
|
|
269
|
+
oauth2Client.setCredentials({
|
|
270
|
+
access_token: envAccessToken,
|
|
271
|
+
refresh_token: envRefreshToken
|
|
272
|
+
});
|
|
273
|
+
if (envRefreshToken) {
|
|
274
|
+
const { credentials: newTokens } = await oauth2Client.refreshAccessToken().catch(() => ({ credentials: null }));
|
|
275
|
+
if (newTokens) oauth2Client.setCredentials(newTokens);
|
|
276
|
+
}
|
|
277
|
+
return oauth2Client;
|
|
278
|
+
}
|
|
279
|
+
const existingTokens = !opts.force ? await loadTokens() : null;
|
|
280
|
+
let refreshFailed = false;
|
|
281
|
+
let refreshError = null;
|
|
282
|
+
if (existingTokens) {
|
|
283
|
+
oauth2Client.setCredentials(existingTokens);
|
|
284
|
+
if (existingTokens.expiry_date && existingTokens.expiry_date < Date.now()) {
|
|
285
|
+
const result = await oauth2Client.refreshAccessToken().then((r) => ({
|
|
286
|
+
credentials: r.credentials,
|
|
287
|
+
error: null
|
|
288
|
+
})).catch((e) => ({
|
|
289
|
+
credentials: null,
|
|
290
|
+
error: e
|
|
291
|
+
}));
|
|
292
|
+
if (result.credentials) {
|
|
293
|
+
await saveTokens(result.credentials);
|
|
294
|
+
oauth2Client.setCredentials(result.credentials);
|
|
295
|
+
if (interactive) logger.success("Token refreshed");
|
|
296
|
+
return oauth2Client;
|
|
297
|
+
}
|
|
298
|
+
refreshFailed = true;
|
|
299
|
+
refreshError = result.error;
|
|
300
|
+
} else {
|
|
301
|
+
if (interactive) logger.success("Using saved credentials");
|
|
302
|
+
return oauth2Client;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!interactive) {
|
|
306
|
+
if (refreshFailed) {
|
|
307
|
+
logger.error(`Token refresh failed${refreshError ? `: ${refreshError.message}` : ""}`);
|
|
308
|
+
logger.info("Refresh token may be revoked or expired. Run `gscdump auth login` to re-authenticate.");
|
|
309
|
+
} else {
|
|
310
|
+
logger.error("Not authenticated");
|
|
311
|
+
logger.info("Run `gscdump auth login` (or `gscdump init` for full setup).");
|
|
312
|
+
}
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
if (opts.noBrowser) {
|
|
316
|
+
const tokens = await authenticateDeviceCode(credentials);
|
|
317
|
+
oauth2Client.setCredentials(tokens);
|
|
318
|
+
await saveTokens(tokens);
|
|
319
|
+
logger.success(`Tokens saved to ${displayPath(getTokensPath())}`);
|
|
320
|
+
return oauth2Client;
|
|
321
|
+
}
|
|
322
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
323
|
+
access_type: "offline",
|
|
324
|
+
scope: SCOPES,
|
|
325
|
+
prompt: "consent"
|
|
326
|
+
});
|
|
327
|
+
logger.info("Waiting for authorization...");
|
|
328
|
+
const { code, redirectUri } = await getAuthCodeViaLoopback(authUrl);
|
|
329
|
+
const { tokens } = await new OAuth2Client(credentials.clientId, credentials.clientSecret, redirectUri).getToken(code);
|
|
330
|
+
oauth2Client.setCredentials(tokens);
|
|
331
|
+
await saveTokens(tokens);
|
|
332
|
+
logger.success(`Tokens saved to ${displayPath(getTokensPath())}`);
|
|
333
|
+
return oauth2Client;
|
|
334
|
+
}
|
|
335
|
+
async function getAuth(opts = {}) {
|
|
336
|
+
const { interactive = true, noBrowser = false, force = false } = opts;
|
|
337
|
+
return authenticate(await getAuthCredentials(interactive), interactive, {
|
|
338
|
+
noBrowser,
|
|
339
|
+
force
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async function resolveAuth(opts = {}) {
|
|
343
|
+
const sa = await resolveServiceAccount({ path: opts.serviceAccount });
|
|
344
|
+
if (sa) {
|
|
345
|
+
logger.success("Using service-account credentials");
|
|
346
|
+
return sa;
|
|
347
|
+
}
|
|
348
|
+
const byok = resolveBYOK(opts.byok);
|
|
349
|
+
if (byok) {
|
|
350
|
+
if (typeof byok !== "string") logger.success("Using BYOK credentials");
|
|
351
|
+
return byok;
|
|
352
|
+
}
|
|
353
|
+
return getAuth(opts);
|
|
354
|
+
}
|
|
355
|
+
function envSourceLabel(envVar) {
|
|
356
|
+
return getAppliedEnvKeys().has(envVar) ? `.env (${envVar})` : `shell env (${envVar})`;
|
|
357
|
+
}
|
|
358
|
+
function pickEnvSource(...envVars) {
|
|
359
|
+
return pickCliEnvironmentValue(envVars);
|
|
360
|
+
}
|
|
361
|
+
function redactCred(v, keepTail = 6) {
|
|
362
|
+
if (!v) return "<unset>";
|
|
363
|
+
if (v.length <= keepTail) return "***";
|
|
364
|
+
return `***${v.slice(-keepTail)}`;
|
|
365
|
+
}
|
|
366
|
+
async function describeAuthProvenance() {
|
|
367
|
+
const rows = [];
|
|
368
|
+
const warnings = [];
|
|
369
|
+
const env = resolveCliEnvironment();
|
|
370
|
+
const saEnvPath = env.serviceAccountPath;
|
|
371
|
+
const saConfigPath = !saEnvPath ? (await loadConfig().catch(() => null))?.serviceAccountPath : void 0;
|
|
372
|
+
const saPath = saEnvPath || saConfigPath;
|
|
373
|
+
if (saEnvPath) {
|
|
374
|
+
const saEnv = env.values.GSC_SERVICE_ACCOUNT_JSON ? "GSC_SERVICE_ACCOUNT_JSON" : "GOOGLE_APPLICATION_CREDENTIALS";
|
|
375
|
+
rows.push({
|
|
376
|
+
field: "service_account",
|
|
377
|
+
source: envSourceLabel(saEnv),
|
|
378
|
+
value: displayPath(saEnvPath)
|
|
379
|
+
});
|
|
380
|
+
} else if (saConfigPath) rows.push({
|
|
381
|
+
field: "service_account",
|
|
382
|
+
source: `${displayPath(`${getConfigDir()}/config.json`)}`,
|
|
383
|
+
value: displayPath(saConfigPath)
|
|
384
|
+
});
|
|
385
|
+
const clientId = pickEnvSource("GSC_CLIENT_ID", "GOOGLE_CLIENT_ID");
|
|
386
|
+
const clientSecret = pickEnvSource("GSC_CLIENT_SECRET", "GOOGLE_CLIENT_SECRET");
|
|
387
|
+
const config = await loadConfig().catch(() => null);
|
|
388
|
+
if (clientId) rows.push({
|
|
389
|
+
field: "client_id",
|
|
390
|
+
source: envSourceLabel(clientId.envVar),
|
|
391
|
+
value: clientId.value
|
|
392
|
+
});
|
|
393
|
+
else if (config?.clientId) rows.push({
|
|
394
|
+
field: "client_id",
|
|
395
|
+
source: `${displayPath(`${getConfigDir()}/config.json`)}`,
|
|
396
|
+
value: config.clientId
|
|
397
|
+
});
|
|
398
|
+
else rows.push({
|
|
399
|
+
field: "client_id",
|
|
400
|
+
source: "<none>",
|
|
401
|
+
value: null
|
|
402
|
+
});
|
|
403
|
+
if (clientSecret) rows.push({
|
|
404
|
+
field: "client_secret",
|
|
405
|
+
source: envSourceLabel(clientSecret.envVar),
|
|
406
|
+
value: redactCred(clientSecret.value)
|
|
407
|
+
});
|
|
408
|
+
else if (config?.clientSecret) rows.push({
|
|
409
|
+
field: "client_secret",
|
|
410
|
+
source: `${displayPath(`${getConfigDir()}/config.json`)}`,
|
|
411
|
+
value: redactCred(config.clientSecret)
|
|
412
|
+
});
|
|
413
|
+
else rows.push({
|
|
414
|
+
field: "client_secret",
|
|
415
|
+
source: "<none>",
|
|
416
|
+
value: null
|
|
417
|
+
});
|
|
418
|
+
const accessTok = pickEnvSource("GSC_ACCESS_TOKEN", "GOOGLE_ACCESS_TOKEN");
|
|
419
|
+
const refreshTok = pickEnvSource("GSC_REFRESH_TOKEN", "GOOGLE_REFRESH_TOKEN");
|
|
420
|
+
if (accessTok) rows.push({
|
|
421
|
+
field: "access_token",
|
|
422
|
+
source: envSourceLabel(accessTok.envVar),
|
|
423
|
+
value: redactCred(accessTok.value)
|
|
424
|
+
});
|
|
425
|
+
if (refreshTok) rows.push({
|
|
426
|
+
field: "refresh_token",
|
|
427
|
+
source: envSourceLabel(refreshTok.envVar),
|
|
428
|
+
value: redactCred(refreshTok.value)
|
|
429
|
+
});
|
|
430
|
+
const tokens = await loadTokens();
|
|
431
|
+
if (tokens) {
|
|
432
|
+
const expiry = tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : "no expiry";
|
|
433
|
+
rows.push({
|
|
434
|
+
field: "saved_tokens",
|
|
435
|
+
source: displayPath(getTokensPath()),
|
|
436
|
+
value: `access=${tokens.access_token ? "present" : "missing"}, refresh=${tokens.refresh_token ? "present" : "missing"}, expiry=${expiry}`
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
let effective;
|
|
440
|
+
if (saPath) effective = "service-account";
|
|
441
|
+
else if (clientId && clientSecret && refreshTok) effective = "byok-refresh-token";
|
|
442
|
+
else if (accessTok) effective = "byok-access-token";
|
|
443
|
+
else if (tokens) effective = "saved-tokens";
|
|
444
|
+
else effective = "none";
|
|
445
|
+
if ((effective === "byok-refresh-token" || effective === "byok-access-token") && tokens) warnings.push("BYOK env vars shadow saved tokens. If you just ran `auth login --force`, the env tokens are stale; unset them or update them.");
|
|
446
|
+
if (effective === "byok-refresh-token" && clientId && refreshTok) {
|
|
447
|
+
if (getAppliedEnvKeys().has(clientId.envVar) !== getAppliedEnvKeys().has(refreshTok.envVar)) warnings.push(`client_id and refresh_token come from different sources (${envSourceLabel(clientId.envVar)} vs ${envSourceLabel(refreshTok.envVar)}); they may not match.`);
|
|
448
|
+
}
|
|
449
|
+
const envFile = getLoadedEnvPath();
|
|
450
|
+
if (envFile && getAppliedEnvKeys().size > 0) warnings.push(`Loaded ${getAppliedEnvKeys().size} var(s) from ${displayPath(envFile)}.`);
|
|
451
|
+
return {
|
|
452
|
+
rows,
|
|
453
|
+
effective,
|
|
454
|
+
warnings
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function formatAuthProvenance() {
|
|
458
|
+
const { rows, effective, warnings } = await describeAuthProvenance();
|
|
459
|
+
const lines = [];
|
|
460
|
+
lines.push(` \x1B[1mAuth config sources\x1B[0m \x1B[90m(effective: ${effective})\x1B[0m`);
|
|
461
|
+
for (const r of rows) {
|
|
462
|
+
const val = r.value ? ` \x1B[90m${r.value}\x1B[0m` : "";
|
|
463
|
+
lines.push(` ${r.field.padEnd(16)} \x1B[36m${r.source}\x1B[0m${val}`);
|
|
464
|
+
}
|
|
465
|
+
if (warnings.length > 0) {
|
|
466
|
+
lines.push("");
|
|
467
|
+
for (const w of warnings) lines.push(` \x1B[33m!\x1B[0m ${w}`);
|
|
468
|
+
}
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
function isAuthError(err) {
|
|
472
|
+
const msg = (err instanceof Error ? err.message : String(err ?? "")).toLowerCase();
|
|
473
|
+
if (!msg) return false;
|
|
474
|
+
return /\b(?:401|403|unauthorized|forbidden|invalid_grant|invalid_token|insufficient.*scope|invalid_client|token has been expired|token has been revoked)\b/.test(msg) || msg.includes("oauth2.googleapis.com/token");
|
|
475
|
+
}
|
|
476
|
+
export { authenticate, clearTokens, formatAuthProvenance, getAuth, getAuthCredentials, isAuthError, loadServiceAccount, loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount, saveTokens };
|