From bc2b56160c041caba48006a2375a280ee2dc1d2f Mon Sep 17 00:00:00 2001 From: m4p1x Date: Sun, 12 Apr 2026 14:47:00 +0200 Subject: [PATCH 1/4] fix(auth): replace local-auth with manual localhost callback server - Remove @google-cloud/local-auth dependency (calls open() unconditionally) - Remove OOB flow (deprecated/blocked by Google since 2022) - Implement localhost HTTP server on ephemeral port for OAuth callback - Add server.unref() to prevent process hanging after successful auth - Works in headless/container environments (toolbox on Silverblue) --- src/api/auth.ts | 83 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index 78041cf..b78cac8 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -4,9 +4,8 @@ import { google } from 'googleapis'; import { OAuth2Client } from 'google-auth-library'; -import { authenticate } from '@google-cloud/local-auth'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import open from 'open'; +import { existsSync, readFileSync } from 'fs'; +import * as http from 'http'; import config from '../utils/config.js'; import { AuthError } from '../utils/errors.js'; @@ -82,6 +81,8 @@ export async function getAuthClient(): Promise { /** * Perform interactive OAuth login flow + * Prints the auth URL to stdout and waits for the authorization code + * (works in headless/container environments without a browser) */ export async function login(): Promise { const credentialsPath = config.getCredentialsPath(); @@ -100,26 +101,72 @@ export async function login(): Promise { ); } - try { - const localClient = await authenticate({ - scopes: SCOPES, - keyfilePath: credentialsPath, + const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8')); + const { client_id, client_secret } = credentials.installed || credentials.web; + + // Shared state captured across the server lifecycle + let capturedClient: OAuth2Client | null = null; + let capturedCode: string | null = null; + + await new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const url = new URL(req.url!, `http://localhost`); + const code = url.searchParams.get('code'); + const error = url.searchParams.get('error'); + + if (error) { + res.writeHead(400, { 'Content-Type': 'text/html' }); + res.end('

Authentication failed

You can close this tab.

'); + server.close(() => reject(new Error(`OAuth error: ${error}`))); + return; + } + + if (code) { + capturedCode = code; + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('

Authentication successful!

You can close this tab and return to the terminal.

'); + server.close(() => resolve()); + } }); - if (localClient.credentials) { - saveToken(localClient.credentials); - const client = createOAuthClient(credentialsPath); - client.setCredentials(localClient.credentials); - cachedClient = client; - return client; - } + server.listen(0, '127.0.0.1', () => { + server.unref(); // Don't keep process alive waiting for more connections + const port = (server.address() as { port: number }).port; + const redirectUri = `http://localhost:${port}`; + capturedClient = new OAuth2Client(client_id, client_secret, redirectUri); + + const authUrl = capturedClient.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES, + prompt: 'consent', + redirect_uri: redirectUri, + }); + + console.log('\nOpen this URL in your browser to authenticate:\n'); + console.log(authUrl); + console.log('\nWaiting for callback on', redirectUri, '...'); + }); - const client = createOAuthClient(credentialsPath); - cachedClient = client; - return client; + server.on('error', (err) => reject(err)); + }); + + if (!capturedClient || !capturedCode) { + throw new AuthError('Auth flow did not complete.', 'Try running "gdocs auth login" again.'); + } + + const finalClient = capturedClient as OAuth2Client; + const finalCode = capturedCode as string; + + try { + const { tokens } = await finalClient.getToken(finalCode); + finalClient.setCredentials(tokens); + saveToken(tokens); + cachedClient = finalClient; + console.log('\nAuthentication successful!'); + return finalClient; } catch (error) { throw new AuthError( - `Authentication failed: ${error instanceof Error ? error.message : String(error)}`, + `Token exchange failed: ${error instanceof Error ? error.message : String(error)}`, 'Ensure your credentials file is valid and try again.' ); } From f470e817f29f003f1f151c4ae0acc3565b9aece1 Mon Sep 17 00:00:00 2001 From: m4p1x Date: Sun, 12 Apr 2026 18:03:48 +0200 Subject: [PATCH 2/4] refactor(auth): add timeout, remove helper, improve type safety - Remove createOAuthClient() helper (was only used once, inline it) - Add 5-minute timeout on OAuth callback server - Use tuple cell pattern to avoid TypeScript closure narrowing issues - Improve server.address() type narrowing (reject on string/null addr) - Add timeout message to user-facing output --- src/api/auth.ts | 68 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index b78cac8..aaa47d1 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -16,13 +16,9 @@ const SCOPES = [ 'https://www.googleapis.com/auth/drive.readonly', ]; -let cachedClient: OAuth2Client | null = null; +const LOGIN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes -function createOAuthClient(credentialsPath: string): OAuth2Client { - const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8')); - const { client_id, client_secret, redirect_uris } = credentials.installed || credentials.web; - return new OAuth2Client(client_id, client_secret, redirect_uris[0]); -} +let cachedClient: OAuth2Client | null = null; /** * Get an authenticated OAuth2 client @@ -47,7 +43,9 @@ export async function getAuthClient(): Promise { if (existsSync(tokenPath)) { try { const token = JSON.parse(readFileSync(tokenPath, 'utf-8')); - const client = createOAuthClient(credentialsPath); + const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8')); + const { client_id, client_secret, redirect_uris } = credentials.installed || credentials.web; + const client = new OAuth2Client(client_id, client_secret, redirect_uris[0]); client.setCredentials(token); // Check if token is expired and refresh if needed @@ -80,9 +78,9 @@ export async function getAuthClient(): Promise { } /** - * Perform interactive OAuth login flow - * Prints the auth URL to stdout and waits for the authorization code - * (works in headless/container environments without a browser) + * Perform interactive OAuth login flow using a local HTTP callback server. + * Works in headless/container environments — the server runs on localhost, + * which is reachable from the host browser (shared network namespace). */ export async function login(): Promise { const credentialsPath = config.getCredentialsPath(); @@ -104,11 +102,15 @@ export async function login(): Promise { const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8')); const { client_id, client_secret } = credentials.installed || credentials.web; - // Shared state captured across the server lifecycle - let capturedClient: OAuth2Client | null = null; - let capturedCode: string | null = null; + // TypeScript cannot track mutations made inside Promise closures, so we use + // a single-element array as a mutable cell with a stable reference. + // After the Promise resolves these are guaranteed to be set. + const clientCell: [OAuth2Client | null] = [null]; + const codeCell: [string | null] = [null]; await new Promise((resolve, reject) => { + let timer: ReturnType | null = null; + const server = http.createServer((req, res) => { const url = new URL(req.url!, `http://localhost`); const code = url.searchParams.get('code'); @@ -117,25 +119,36 @@ export async function login(): Promise { if (error) { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end('

Authentication failed

You can close this tab.

'); - server.close(() => reject(new Error(`OAuth error: ${error}`))); + if (timer) clearTimeout(timer); + server.close(() => reject(new AuthError(`OAuth error: ${error}`, 'Try running "gdocs auth login" again.'))); return; } if (code) { - capturedCode = code; + codeCell[0] = code; res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('

Authentication successful!

You can close this tab and return to the terminal.

'); + if (timer) clearTimeout(timer); server.close(() => resolve()); } }); server.listen(0, '127.0.0.1', () => { - server.unref(); // Don't keep process alive waiting for more connections - const port = (server.address() as { port: number }).port; + // server.unref() prevents Node.js from keeping the process alive + // after the token is saved and server.close() is called. + server.unref(); + + const addr = server.address(); + if (!addr || typeof addr === 'string') { + reject(new AuthError('Failed to start local auth server.', 'Try running "gdocs auth login" again.')); + return; + } + + const port = addr.port; const redirectUri = `http://localhost:${port}`; - capturedClient = new OAuth2Client(client_id, client_secret, redirectUri); + clientCell[0] = new OAuth2Client(client_id, client_secret, redirectUri); - const authUrl = capturedClient.generateAuthUrl({ + const authUrl = clientCell[0]!.generateAuthUrl({ access_type: 'offline', scope: SCOPES, prompt: 'consent', @@ -145,17 +158,28 @@ export async function login(): Promise { console.log('\nOpen this URL in your browser to authenticate:\n'); console.log(authUrl); console.log('\nWaiting for callback on', redirectUri, '...'); + console.log(`(timeout in ${LOGIN_TIMEOUT_MS / 60000} minutes)`); + + timer = setTimeout(() => { + server.close(() => + reject(new AuthError( + 'Authentication timed out.', + `No response received within ${LOGIN_TIMEOUT_MS / 60000} minutes. Run "gdocs auth login" to try again.` + )) + ); + }, LOGIN_TIMEOUT_MS); }); server.on('error', (err) => reject(err)); }); - if (!capturedClient || !capturedCode) { + // clientCell[0] and codeCell[0] are set before resolve() is called. + if (clientCell[0] === null || codeCell[0] === null) { throw new AuthError('Auth flow did not complete.', 'Try running "gdocs auth login" again.'); } - const finalClient = capturedClient as OAuth2Client; - const finalCode = capturedCode as string; + const finalClient = clientCell[0]; + const finalCode = codeCell[0]; try { const { tokens } = await finalClient.getToken(finalCode); From 580ba9d1739867754e1caae02c7a6747f59c9188 Mon Sep 17 00:00:00 2001 From: m4p1x Date: Sun, 12 Apr 2026 18:17:56 +0200 Subject: [PATCH 3/4] fix(auth): close keep-alive connections so server.close() callback fires Browser holds HTTP keep-alive connection open after OAuth redirect. server.close() only fires its callback once all connections close, so the Promise never resolved. Fix by calling closeAllConnections() before server.close() to drop open connections immediately. --- src/api/auth.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index aaa47d1..aaad1e6 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -120,6 +120,7 @@ export async function login(): Promise { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end('

Authentication failed

You can close this tab.

'); if (timer) clearTimeout(timer); + server.closeAllConnections?.(); server.close(() => reject(new AuthError(`OAuth error: ${error}`, 'Try running "gdocs auth login" again.'))); return; } @@ -129,16 +130,15 @@ export async function login(): Promise { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('

Authentication successful!

You can close this tab and return to the terminal.

'); if (timer) clearTimeout(timer); + // closeAllConnections() drops keep-alive connections immediately so + // server.close() fires its callback without waiting for the browser. + server.closeAllConnections?.(); server.close(() => resolve()); + return; } }); - server.listen(0, '127.0.0.1', () => { - // server.unref() prevents Node.js from keeping the process alive - // after the token is saved and server.close() is called. - server.unref(); - - const addr = server.address(); + server.listen(0, '127.0.0.1', () => { const addr = server.address(); if (!addr || typeof addr === 'string') { reject(new AuthError('Failed to start local auth server.', 'Try running "gdocs auth login" again.')); return; From fd29300bc4814f09e8cf1483a8ef64dc6c72b35d Mon Sep 17 00:00:00 2001 From: m4p1x Date: Sun, 12 Apr 2026 18:40:32 +0200 Subject: [PATCH 4/4] chore(deps): remove unused @google-cloud/local-auth dependency Replaced by manual localhost HTTP server in auth.ts (bc2b561). The package is no longer imported anywhere in the codebase. --- package-lock.json | 212 ---------------------------------------------- package.json | 1 - 2 files changed, 213 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2db9161..4a07db1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@google-cloud/local-auth": "^3.0.1", "chalk": "^5.3.0", "cli-table3": "^0.6.5", "commander": "^12.1.0", @@ -484,153 +483,6 @@ "node": ">=18" } }, - "node_modules/@google-cloud/local-auth": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/local-auth/-/local-auth-3.0.1.tgz", - "integrity": "sha512-YJ3GFbksfHyEarbVHPSCzhKpjbnlAhdzg2SEf79l6ODukrSM1qUOqfopY232Xkw26huKSndyzmJz+A6b2WYn7Q==", - "license": "Apache-2.0", - "dependencies": { - "arrify": "^2.0.1", - "google-auth-library": "^9.0.0", - "open": "^7.0.3", - "server-destroy": "^1.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@google-cloud/local-auth/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google-cloud/local-auth/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1009,7 +861,6 @@ "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1193,15 +1044,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2109,18 +1951,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -2696,12 +2526,6 @@ "node": ">=10" } }, - "node_modules/server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3017,12 +2841,6 @@ "node": ">=14.0.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -3094,26 +2912,12 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "license": "BSD" }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3696,22 +3500,6 @@ "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", diff --git a/package.json b/package.json index 50c6dd4..5484264 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "node": ">=18.0.0" }, "dependencies": { - "@google-cloud/local-auth": "^3.0.1", "chalk": "^5.3.0", "cli-table3": "^0.6.5", "commander": "^12.1.0",