google-apis-cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/credentials.js +67 -31
- package/package.json +1 -1
package/dist/src/credentials.js
CHANGED
|
@@ -38,7 +38,18 @@ export function openCredentials({ dir, env, oauth = (opts) => new OAuth2Client(o
|
|
|
38
38
|
let apiKey;
|
|
39
39
|
let tokenClient;
|
|
40
40
|
let saving; // the refreshed-token write the 'tokens' event starts
|
|
41
|
-
|
|
41
|
+
// Each prompting command reads stdin through one session, closed when it ends (TTY raw mode).
|
|
42
|
+
const withPrompt = async (given, fn) => {
|
|
43
|
+
if (given)
|
|
44
|
+
return fn(given);
|
|
45
|
+
const p = openPrompt({ input: stdin, output: stderr });
|
|
46
|
+
try {
|
|
47
|
+
return await fn(p.ask);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
p.close();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
42
53
|
// Only a missing file reads as {}: anything else stops before `setup` can overwrite it.
|
|
43
54
|
// The parse error is not echoed, since V8 quotes the file text (secrets) in it.
|
|
44
55
|
const readConfig = async () => {
|
|
@@ -171,7 +182,7 @@ export function openCredentials({ dir, env, oauth = (opts) => new OAuth2Client(o
|
|
|
171
182
|
return token ? hash('sha256', token) : undefined;
|
|
172
183
|
}
|
|
173
184
|
// Both flows send `state` and a PKCE S256 challenge, and redeem one redirect URL.
|
|
174
|
-
async function login({ consent, manual = false, prompt
|
|
185
|
+
async function login({ consent, manual = false, prompt, }) {
|
|
175
186
|
const c = await client(REDIRECT_URI);
|
|
176
187
|
const state = randomBytes(32).toString('base64url');
|
|
177
188
|
const { codeVerifier, codeChallenge } = await c.generateCodeVerifierAsync();
|
|
@@ -206,7 +217,8 @@ export function openCredentials({ dir, env, oauth = (opts) => new OAuth2Client(o
|
|
|
206
217
|
stderr.write(`Open this URL and grant access:\n${url}\n` +
|
|
207
218
|
`After consent the browser cannot load ${LOOPBACK_HOST}:${LOOPBACK_PORT}; ` +
|
|
208
219
|
'paste the full URL from its address bar.\n');
|
|
209
|
-
const
|
|
220
|
+
const answer = await withPrompt(prompt, (ask) => ask('redirected URL: '));
|
|
221
|
+
const redirect = URL.parse(answer.trim());
|
|
210
222
|
if (!redirect)
|
|
211
223
|
throw new GapiError(2, 'not a URL; paste the full URL from the address bar');
|
|
212
224
|
return redeem(redirect);
|
|
@@ -279,7 +291,7 @@ export function openCredentials({ dir, env, oauth = (opts) => new OAuth2Client(o
|
|
|
279
291
|
await rm(credentialsFile, { force: true });
|
|
280
292
|
}
|
|
281
293
|
// `headers`: every entry's header specs, tagged with the entry's api.
|
|
282
|
-
async function setup(headers, prompt
|
|
294
|
+
async function setup(headers, prompt) {
|
|
283
295
|
const cfg = await readConfig();
|
|
284
296
|
const fields = [
|
|
285
297
|
...CLIENT_FIELDS,
|
|
@@ -291,15 +303,17 @@ export function openCredentials({ dir, env, oauth = (opts) => new OAuth2Client(o
|
|
|
291
303
|
})),
|
|
292
304
|
API_KEY_FIELD,
|
|
293
305
|
];
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
306
|
+
await withPrompt(prompt, async (ask) => {
|
|
307
|
+
for (const { key, label, required, secret } of fields) {
|
|
308
|
+
const cur = cfg[key];
|
|
309
|
+
const hint = cur != null ? ' [keep current]' : required ? '' : ' (blank to skip)';
|
|
310
|
+
const v = (await ask(`${label}${hint}: `, { secret })).trim();
|
|
311
|
+
if (v)
|
|
312
|
+
cfg[key] = v;
|
|
313
|
+
else if (required && cur == null)
|
|
314
|
+
throw new GapiError(3, `${label} is required`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
303
317
|
await writeSecure(configFile, cfg);
|
|
304
318
|
return configFile;
|
|
305
319
|
}
|
|
@@ -318,25 +332,47 @@ function accountFromIdToken(idToken) {
|
|
|
318
332
|
return null;
|
|
319
333
|
}
|
|
320
334
|
}
|
|
321
|
-
// `secret: true` keeps the typed value off the screen. Node has no masking
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
export async function defaultPrompt(q, { secret = false, input, output, terminal, }) {
|
|
327
|
-
if (secret)
|
|
328
|
-
output.write(q);
|
|
335
|
+
// `secret: true` keeps the typed value off the screen. Node has no masking option.
|
|
336
|
+
// So readline echoes through a gate that drops writes during a secret.
|
|
337
|
+
// TTY raw mode stops the driver echo; a pipe echoes nothing.
|
|
338
|
+
export function openPrompt({ input, output, terminal, }) {
|
|
339
|
+
let muted = false; // a secret: drop the terminal echo of what the user types
|
|
329
340
|
const rl = createInterface({
|
|
330
341
|
input,
|
|
331
|
-
output:
|
|
332
|
-
|
|
342
|
+
output: new Writable({
|
|
343
|
+
write: (c, _e, cb) => {
|
|
344
|
+
if (!muted)
|
|
345
|
+
output.write(c);
|
|
346
|
+
cb();
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
terminal: terminal ?? ('isTTY' in input && input.isTTY === true),
|
|
333
350
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
// One reader per session, made before any await: a pipe sends all answers in one chunk (#233).
|
|
352
|
+
const lines = rl[Symbol.asyncIterator]();
|
|
353
|
+
let ended = false; // input closed: buffered lines remain, but `rl.prompt` throws
|
|
354
|
+
rl.on('close', () => {
|
|
355
|
+
ended = true;
|
|
356
|
+
});
|
|
357
|
+
const ask = async (q, { secret = false } = {}) => {
|
|
358
|
+
muted = secret;
|
|
359
|
+
if (secret || ended)
|
|
360
|
+
output.write(q);
|
|
361
|
+
else {
|
|
362
|
+
rl.setPrompt(q); // terminal mode redraws the prompt on line edits
|
|
363
|
+
rl.prompt();
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const { value, done } = await lines.next();
|
|
367
|
+
if (done)
|
|
368
|
+
throw new GapiError(3, `input ended before an answer to: ${q.trim()}`);
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
muted = false;
|
|
373
|
+
if (secret)
|
|
374
|
+
output.write('\n'); // the swallowed Enter (TTY) or the pipe never printed one
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
return { ask, close: () => rl.close() };
|
|
342
378
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-apis-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "One discovery-driven CLI (gapi) for 49 Google marketing and data APIs: Ads, GA4, Search Console, Tag Manager, Indexing, Merchant, Business Profile, SA360, CM360, DV360, YouTube, PageSpeed, CrUX, BigQuery, Sheets, Places and more. One login, read+write.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|