@dguido/google-workspace-mcp 3.0.0 → 3.0.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/index.js CHANGED
@@ -989,6 +989,30 @@ function getKeysFilePath() {
989
989
  }
990
990
  return path.join(process.cwd(), "gcp-oauth.keys.json");
991
991
  }
992
+ function extractCredentials(keys) {
993
+ if (keys.installed?.client_id) {
994
+ return {
995
+ client_id: keys.installed.client_id,
996
+ client_secret: keys.installed.client_secret,
997
+ redirect_uris: keys.installed.redirect_uris
998
+ };
999
+ }
1000
+ if (keys.web?.client_id) {
1001
+ return {
1002
+ client_id: keys.web.client_id,
1003
+ client_secret: keys.web.client_secret,
1004
+ redirect_uris: keys.web.redirect_uris
1005
+ };
1006
+ }
1007
+ if (keys.client_id) {
1008
+ return {
1009
+ client_id: keys.client_id,
1010
+ client_secret: keys.client_secret,
1011
+ redirect_uris: keys.redirect_uris || ["http://127.0.0.1/oauth2callback"]
1012
+ };
1013
+ }
1014
+ return null;
1015
+ }
992
1016
  function generateCredentialsErrorMessage() {
993
1017
  return `
994
1018
  OAuth credentials not found. Please provide credentials using one of these methods:
@@ -1038,6 +1062,14 @@ var GoogleAuthError = class _GoogleAuthError extends Error {
1038
1062
  Error.captureStackTrace(this, _GoogleAuthError);
1039
1063
  }
1040
1064
  }
1065
+ /** Check if this error indicates the OAuth client is invalid/deleted */
1066
+ isClientInvalid() {
1067
+ return this.code === "DELETED_CLIENT" || this.code === "INVALID_CLIENT";
1068
+ }
1069
+ /** Check if this error requires clearing stored tokens */
1070
+ requiresTokenClear() {
1071
+ return this.code === "INVALID_GRANT" || this.code === "TOKEN_REVOKED" || this.code === "DELETED_CLIENT";
1072
+ }
1041
1073
  /**
1042
1074
  * Convert to structured data for MCP tool response.
1043
1075
  */
@@ -1111,6 +1143,8 @@ function mapGoogleError(error, context = {}) {
1111
1143
  return createRedirectUriMismatchError(description, error, context);
1112
1144
  case "invalid_client":
1113
1145
  return createInvalidClientError(description, error, context);
1146
+ case "deleted_client":
1147
+ return createDeletedClientError(description, error, context);
1114
1148
  case "invalid_grant":
1115
1149
  return createInvalidGrantError(description, error, context);
1116
1150
  case "access_denied":
@@ -1178,6 +1212,22 @@ function createInvalidClientError(description, originalError, context) {
1178
1212
  ...context
1179
1213
  });
1180
1214
  }
1215
+ function createDeletedClientError(description, originalError, context) {
1216
+ return new GoogleAuthError({
1217
+ code: "DELETED_CLIENT",
1218
+ reason: `OAuth client has been deleted from Google Cloud: ${description}`,
1219
+ fix: [
1220
+ "The OAuth client in your credentials file no longer exists in Google Cloud",
1221
+ "Go to Google Cloud Console > APIs & Services > Credentials",
1222
+ "Create a new OAuth 2.0 Client ID (Desktop app type)",
1223
+ "Download and save as gcp-oauth.keys.json",
1224
+ "Delete existing tokens and re-authenticate"
1225
+ ],
1226
+ links: [{ label: "Create OAuth Credentials", url: `${CONSOLE_URL}/apis/credentials` }],
1227
+ originalError,
1228
+ ...context
1229
+ });
1230
+ }
1181
1231
  function createInvalidGrantError(description, originalError, context) {
1182
1232
  return new GoogleAuthError({
1183
1233
  code: "INVALID_GRANT",
@@ -1377,8 +1427,9 @@ async function validateOAuthConfig() {
1377
1427
  );
1378
1428
  return { valid: false, errors, warnings };
1379
1429
  }
1380
- const clientId = credentials.installed?.client_id || credentials.web?.client_id || credentials.client_id;
1381
- const clientSecret = credentials.installed?.client_secret || credentials.web?.client_secret || credentials.client_secret;
1430
+ const extracted = extractCredentials(credentials);
1431
+ const clientId = extracted?.client_id;
1432
+ const clientSecret = extracted?.client_secret;
1382
1433
  if (!clientId) {
1383
1434
  errors.push(
1384
1435
  new GoogleAuthError({
@@ -1484,29 +1535,13 @@ function parseStoredCredentials(content) {
1484
1535
  async function loadCredentialsFromFile() {
1485
1536
  const keysContent = await fs2.readFile(getKeysFilePath(), "utf-8");
1486
1537
  const keys = parseCredentialsFile(keysContent);
1487
- if (keys.installed?.client_id) {
1488
- return {
1489
- client_id: keys.installed.client_id,
1490
- client_secret: keys.installed.client_secret,
1491
- redirect_uris: keys.installed.redirect_uris
1492
- };
1493
- } else if (keys.web?.client_id) {
1494
- return {
1495
- client_id: keys.web.client_id,
1496
- client_secret: keys.web.client_secret,
1497
- redirect_uris: keys.web.redirect_uris
1498
- };
1499
- } else if (keys.client_id) {
1500
- return {
1501
- client_id: keys.client_id,
1502
- client_secret: keys.client_secret,
1503
- redirect_uris: keys.redirect_uris || ["http://127.0.0.1/oauth2callback"]
1504
- };
1505
- } else {
1538
+ const credentials = extractCredentials(keys);
1539
+ if (!credentials) {
1506
1540
  throw new Error(
1507
1541
  'Invalid credentials file format. Expected either "installed", "web" object or direct client_id field.'
1508
1542
  );
1509
1543
  }
1544
+ return credentials;
1510
1545
  }
1511
1546
  async function loadCredentialsWithFallback() {
1512
1547
  try {
@@ -1517,21 +1552,11 @@ async function loadCredentialsWithFallback() {
1517
1552
  const legacyContent = await fs2.readFile(legacyPath, "utf-8");
1518
1553
  const legacyKeys = parseCredentialsFile(legacyContent);
1519
1554
  log("Warning: Using legacy client_secret.json. Please migrate to gcp-oauth.keys.json");
1520
- if (legacyKeys.installed?.client_id) {
1521
- return {
1522
- client_id: legacyKeys.installed.client_id,
1523
- client_secret: legacyKeys.installed.client_secret,
1524
- redirect_uris: legacyKeys.installed.redirect_uris
1525
- };
1526
- } else if (legacyKeys.web?.client_id) {
1527
- return {
1528
- client_id: legacyKeys.web.client_id,
1529
- client_secret: legacyKeys.web.client_secret,
1530
- redirect_uris: legacyKeys.web.redirect_uris
1531
- };
1532
- } else {
1555
+ const credentials = extractCredentials(legacyKeys);
1556
+ if (!credentials) {
1533
1557
  throw new Error("Invalid legacy credentials format");
1534
1558
  }
1559
+ return credentials;
1535
1560
  } catch {
1536
1561
  const errorMessage = generateCredentialsErrorMessage();
1537
1562
  throw new Error(
@@ -1592,21 +1617,33 @@ import { CodeChallengeMethod, OAuth2Client as OAuth2Client2 } from "google-auth-
1592
1617
  // src/auth/tokenManager.ts
1593
1618
  import * as fs3 from "fs/promises";
1594
1619
  import * as path2 from "path";
1620
+ import { Mutex } from "async-mutex";
1595
1621
  init_utils();
1596
- var lastAuthError = null;
1622
+ var activeTokenManager = null;
1597
1623
  function getLastTokenAuthError() {
1598
- return lastAuthError;
1624
+ return activeTokenManager?.getLastError() ?? null;
1599
1625
  }
1600
1626
  var TokenManager = class {
1601
1627
  oauth2Client;
1602
1628
  tokenPath;
1603
1629
  accountEmail;
1604
1630
  refreshInProgress = null;
1631
+ lastAuthError = null;
1632
+ tokenFileMutex = new Mutex();
1605
1633
  constructor(oauth2Client, accountEmail) {
1606
1634
  this.oauth2Client = oauth2Client;
1607
1635
  this.tokenPath = getSecureTokenPath();
1608
1636
  this.accountEmail = accountEmail;
1609
1637
  this.setupTokenRefresh();
1638
+ activeTokenManager = this;
1639
+ }
1640
+ /** Get the last auth error that occurred */
1641
+ getLastError() {
1642
+ return this.lastAuthError;
1643
+ }
1644
+ /** Clear the last auth error */
1645
+ clearLastError() {
1646
+ this.lastAuthError = null;
1610
1647
  }
1611
1648
  /** Method to expose the token path */
1612
1649
  getTokenPath() {
@@ -1634,7 +1671,7 @@ var TokenManager = class {
1634
1671
  async ensureTokenDirectoryExists() {
1635
1672
  try {
1636
1673
  const dir = path2.dirname(this.tokenPath);
1637
- await fs3.mkdir(dir, { recursive: true });
1674
+ await fs3.mkdir(dir, { recursive: true, mode: 448 });
1638
1675
  } catch (error) {
1639
1676
  if (isNodeError(error) && error.code !== "EEXIST") {
1640
1677
  log("Failed to create token directory:", error);
@@ -1644,14 +1681,19 @@ var TokenManager = class {
1644
1681
  }
1645
1682
  /**
1646
1683
  * Sets up automatic token persistence when OAuth2Client emits 'tokens' events.
1647
- *
1648
- * Note: This handler has a potential race condition if multiple token refresh
1649
- * events fire rapidly (the file read/merge/write is not atomic). This is
1650
- * acceptable for single-user CLI usage but would need a mutex for multi-process
1651
- * scenarios.
1684
+ * Uses a mutex to prevent race conditions when multiple refresh events fire rapidly.
1652
1685
  */
1653
1686
  setupTokenRefresh() {
1654
- this.oauth2Client.on("tokens", async (newTokens) => {
1687
+ this.oauth2Client.on("tokens", (newTokens) => {
1688
+ void this.updateTokensFile(newTokens);
1689
+ });
1690
+ }
1691
+ /**
1692
+ * Safely update the token file with new tokens, using mutex to prevent races.
1693
+ * Merges new tokens with existing ones, preserving refresh_token and created_at.
1694
+ */
1695
+ async updateTokensFile(newTokens) {
1696
+ await this.tokenFileMutex.runExclusive(async () => {
1655
1697
  try {
1656
1698
  await this.ensureTokenDirectoryExists();
1657
1699
  const content = await fs3.readFile(this.tokenPath, "utf-8");
@@ -1742,9 +1784,9 @@ var TokenManager = class {
1742
1784
  return true;
1743
1785
  } catch (refreshError) {
1744
1786
  const authError = mapGoogleError(refreshError, { account: this.accountEmail });
1745
- lastAuthError = authError;
1787
+ this.lastAuthError = authError;
1746
1788
  log("Token refresh failed:", authError.toToolResponse());
1747
- if (authError.code === "INVALID_GRANT" || authError.code === "TOKEN_REVOKED") {
1789
+ if (authError.requiresTokenClear()) {
1748
1790
  await this.clearTokens();
1749
1791
  }
1750
1792
  return false;
@@ -1837,10 +1879,148 @@ function getScopesForEnabledServices() {
1837
1879
  return [...scopes];
1838
1880
  }
1839
1881
 
1840
- // src/auth/server.ts
1882
+ // src/auth/templates.ts
1841
1883
  function escapeHtml(str) {
1842
1884
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1843
1885
  }
1886
+ var BASE_STYLES = `
1887
+ body {
1888
+ font-family: sans-serif;
1889
+ display: flex;
1890
+ justify-content: center;
1891
+ align-items: center;
1892
+ min-height: 100vh;
1893
+ background-color: #f4f4f4;
1894
+ margin: 0;
1895
+ padding: 1em;
1896
+ }
1897
+ .container {
1898
+ text-align: center;
1899
+ padding: 2em;
1900
+ background-color: #fff;
1901
+ border-radius: 8px;
1902
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1903
+ max-width: 600px;
1904
+ }
1905
+ `;
1906
+ var SUCCESS_STYLES = `
1907
+ ${BASE_STYLES}
1908
+ h1 { color: #4CAF50; }
1909
+ p { color: #333; margin-bottom: 0.5em; }
1910
+ code {
1911
+ background-color: #eee;
1912
+ padding: 0.2em 0.4em;
1913
+ border-radius: 3px;
1914
+ font-size: 0.9em;
1915
+ }
1916
+ .warning {
1917
+ background-color: #fff3cd;
1918
+ border: 1px solid #ffc107;
1919
+ border-radius: 4px;
1920
+ padding: 1em;
1921
+ margin-top: 1em;
1922
+ }
1923
+ .warning p { color: #856404; margin: 0.3em 0; }
1924
+ `;
1925
+ var ERROR_STYLES = `
1926
+ ${BASE_STYLES}
1927
+ .container { text-align: left; }
1928
+ h1 { color: #F44336; text-align: center; }
1929
+ .error-code {
1930
+ background-color: #ffebee;
1931
+ color: #c62828;
1932
+ padding: 0.5em 1em;
1933
+ border-radius: 4px;
1934
+ font-family: monospace;
1935
+ margin: 1em 0;
1936
+ }
1937
+ .reason { color: #333; margin-bottom: 1.5em; }
1938
+ h2 { color: #1976d2; font-size: 1.1em; margin-top: 1.5em; }
1939
+ ol { padding-left: 1.5em; }
1940
+ li { margin-bottom: 0.5em; color: #555; }
1941
+ ul { padding-left: 1em; list-style: none; }
1942
+ ul li { margin-bottom: 0.3em; }
1943
+ a { color: #1976d2; }
1944
+ `;
1945
+ var CSRF_ERROR_STYLES = `
1946
+ ${BASE_STYLES}
1947
+ h1 { color: #F44336; }
1948
+ `;
1949
+ function renderSuccessPage(tokenPath, gitignoreWarning) {
1950
+ return `
1951
+ <!DOCTYPE html>
1952
+ <html lang="en">
1953
+ <head>
1954
+ <meta charset="UTF-8">
1955
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1956
+ <title>Authentication Successful</title>
1957
+ <style>${SUCCESS_STYLES}</style>
1958
+ </head>
1959
+ <body>
1960
+ <div class="container">
1961
+ <h1>Authentication Successful!</h1>
1962
+ <p>Your authentication tokens have been saved successfully to:</p>
1963
+ <p><code>${escapeHtml(tokenPath)}</code></p>${gitignoreWarning}
1964
+ <p style="margin-top: 1em;">You can now close this browser window.</p>
1965
+ </div>
1966
+ </body>
1967
+ </html>
1968
+ `;
1969
+ }
1970
+ function buildGitignoreWarning(credentialsDir) {
1971
+ return `
1972
+ <div class="warning">
1973
+ <p><strong>\u26A0\uFE0F Security:</strong> Add your credentials directory to .gitignore:</p>
1974
+ <p><code>${escapeHtml(credentialsDir)}/</code></p>
1975
+ </div>`;
1976
+ }
1977
+ function renderErrorPage(authError) {
1978
+ const fixStepsHtml = authError.fix.map((step, i) => `<li>${i + 1}. ${escapeHtml(step)}</li>`).join("");
1979
+ const linksHtml = authError.links ? authError.links.map(
1980
+ (link) => `<li><a href="${escapeHtml(link.url)}" target="_blank">${escapeHtml(link.label)}</a></li>`
1981
+ ).join("") : "";
1982
+ return `
1983
+ <!DOCTYPE html>
1984
+ <html lang="en">
1985
+ <head>
1986
+ <meta charset="UTF-8">
1987
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1988
+ <title>Authentication Failed</title>
1989
+ <style>${ERROR_STYLES}</style>
1990
+ </head>
1991
+ <body>
1992
+ <div class="container">
1993
+ <h1>Authentication Failed</h1>
1994
+ <div class="error-code">${escapeHtml(authError.code)}</div>
1995
+ <p class="reason">${escapeHtml(authError.reason)}</p>
1996
+ <h2>How to fix:</h2>
1997
+ <ol>${fixStepsHtml}</ol>
1998
+ ${linksHtml ? `<h2>Helpful links:</h2><ul>${linksHtml}</ul>` : ""}
1999
+ </div>
2000
+ </body>
2001
+ </html>
2002
+ `;
2003
+ }
2004
+ function renderCsrfErrorPage() {
2005
+ return `
2006
+ <!DOCTYPE html>
2007
+ <html lang="en">
2008
+ <head>
2009
+ <title>Authentication Failed</title>
2010
+ <style>${CSRF_ERROR_STYLES}</style>
2011
+ </head>
2012
+ <body>
2013
+ <div class="container">
2014
+ <h1>Authentication Failed</h1>
2015
+ <p>Invalid state parameter. This may indicate a CSRF attack.</p>
2016
+ <p>Please close this window and try again.</p>
2017
+ </div>
2018
+ </body>
2019
+ </html>
2020
+ `;
2021
+ }
2022
+
2023
+ // src/auth/server.ts
1844
2024
  function timingSafeEqual(a, b) {
1845
2025
  const bufA = Buffer.from(a);
1846
2026
  const bufB = Buffer.from(b);
@@ -1860,154 +2040,94 @@ var AuthServer = class {
1860
2040
  constructor(oauth2Client) {
1861
2041
  this.tokenManager = new TokenManager(oauth2Client);
1862
2042
  }
2043
+ isClientInvalidError() {
2044
+ const lastError = getLastTokenAuthError();
2045
+ return lastError?.isClientInvalid() ?? false;
2046
+ }
2047
+ /** Handle root request - show auth link page */
2048
+ handleRootRequest(res) {
2049
+ if (!this.flowOAuth2Client) {
2050
+ res.writeHead(503, { "Content-Type": "text/plain" });
2051
+ res.end("Authentication server is starting. Please wait and refresh.");
2052
+ return;
2053
+ }
2054
+ if (!this.codeChallenge || !this.expectedState) {
2055
+ res.writeHead(500, { "Content-Type": "text/plain" });
2056
+ res.end("PKCE not initialized - call start() first");
2057
+ return;
2058
+ }
2059
+ const authUrl = this.flowOAuth2Client.generateAuthUrl({
2060
+ access_type: "offline",
2061
+ scope: getScopesForEnabledServices(),
2062
+ prompt: "consent",
2063
+ code_challenge_method: CodeChallengeMethod.S256,
2064
+ code_challenge: this.codeChallenge,
2065
+ state: this.expectedState
2066
+ });
2067
+ res.writeHead(200, { "Content-Type": "text/html" });
2068
+ res.end(
2069
+ `<h1>Google Drive Authentication</h1><a href="${authUrl}">Authenticate with Google</a>`
2070
+ );
2071
+ }
2072
+ /** Handle OAuth callback - exchange code for tokens */
2073
+ async handleOAuthCallback(url, res) {
2074
+ const receivedState = url.searchParams.get("state");
2075
+ if (!this.expectedState || !receivedState || !timingSafeEqual(receivedState, this.expectedState)) {
2076
+ res.writeHead(400, { "Content-Type": "text/html" });
2077
+ res.end(renderCsrfErrorPage());
2078
+ return;
2079
+ }
2080
+ const code = url.searchParams.get("code");
2081
+ if (!code) {
2082
+ res.writeHead(400, { "Content-Type": "text/plain" });
2083
+ res.end("Authorization code missing");
2084
+ return;
2085
+ }
2086
+ if (!this.flowOAuth2Client) {
2087
+ res.writeHead(500, { "Content-Type": "text/plain" });
2088
+ res.end("Authentication flow not properly initiated.");
2089
+ return;
2090
+ }
2091
+ try {
2092
+ const { tokens } = await this.flowOAuth2Client.getToken({
2093
+ code,
2094
+ codeVerifier: this.codeVerifier || void 0
2095
+ });
2096
+ await this.tokenManager.saveTokens(tokens);
2097
+ this.authCompletedSuccessfully = true;
2098
+ this.clearPkceState();
2099
+ setTimeout(() => {
2100
+ this.stop().catch((err) => log("Auth server shutdown error:", err));
2101
+ }, 2e3);
2102
+ const tokenPath = this.tokenManager.getTokenPath();
2103
+ const homeConfig = path3.join(os2.homedir(), ".config");
2104
+ const isProjectLevel = !tokenPath.startsWith(homeConfig);
2105
+ const credentialsDir = path3.basename(path3.dirname(tokenPath));
2106
+ const gitignoreWarning = isProjectLevel ? buildGitignoreWarning(credentialsDir) : "";
2107
+ res.writeHead(200, { "Content-Type": "text/html" });
2108
+ res.end(renderSuccessPage(tokenPath, gitignoreWarning));
2109
+ } catch (error) {
2110
+ this.authCompletedSuccessfully = false;
2111
+ this.clearPkceState();
2112
+ const authError = mapGoogleError(error);
2113
+ log("OAuth callback error:", authError.toToolResponse());
2114
+ res.writeHead(500, { "Content-Type": "text/html" });
2115
+ res.end(renderErrorPage(authError));
2116
+ }
2117
+ }
2118
+ /** Clear sensitive PKCE/state values */
2119
+ clearPkceState() {
2120
+ this.codeVerifier = null;
2121
+ this.codeChallenge = null;
2122
+ this.expectedState = null;
2123
+ }
1863
2124
  createServer() {
1864
2125
  return http.createServer(async (req, res) => {
1865
2126
  const url = new URL(req.url || "/", `http://127.0.0.1`);
1866
2127
  if (url.pathname === "/") {
1867
- if (!this.flowOAuth2Client) {
1868
- res.writeHead(503, { "Content-Type": "text/plain" });
1869
- res.end("Authentication server is starting. Please wait and refresh.");
1870
- return;
1871
- }
1872
- if (!this.codeChallenge || !this.expectedState) {
1873
- res.writeHead(500, { "Content-Type": "text/plain" });
1874
- res.end("PKCE not initialized - call start() first");
1875
- return;
1876
- }
1877
- const authUrl = this.flowOAuth2Client.generateAuthUrl({
1878
- access_type: "offline",
1879
- scope: getScopesForEnabledServices(),
1880
- prompt: "consent",
1881
- code_challenge_method: CodeChallengeMethod.S256,
1882
- code_challenge: this.codeChallenge,
1883
- state: this.expectedState
1884
- });
1885
- res.writeHead(200, { "Content-Type": "text/html" });
1886
- res.end(
1887
- `<h1>Google Drive Authentication</h1><a href="${authUrl}">Authenticate with Google</a>`
1888
- );
2128
+ this.handleRootRequest(res);
1889
2129
  } else if (url.pathname === "/oauth2callback") {
1890
- const receivedState = url.searchParams.get("state");
1891
- if (!this.expectedState || !receivedState || !timingSafeEqual(receivedState, this.expectedState)) {
1892
- res.writeHead(400, { "Content-Type": "text/html" });
1893
- res.end(`
1894
- <!DOCTYPE html>
1895
- <html lang="en">
1896
- <head><title>Authentication Failed</title>
1897
- <style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;background:#f4f4f4;margin:0}.container{text-align:center;padding:2em;background:#fff;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}h1{color:#F44336}</style>
1898
- </head>
1899
- <body><div class="container"><h1>Authentication Failed</h1><p>Invalid state parameter. This may indicate a CSRF attack.</p><p>Please close this window and try again.</p></div></body>
1900
- </html>
1901
- `);
1902
- return;
1903
- }
1904
- const code = url.searchParams.get("code");
1905
- if (!code) {
1906
- res.writeHead(400, { "Content-Type": "text/plain" });
1907
- res.end("Authorization code missing");
1908
- return;
1909
- }
1910
- if (!this.flowOAuth2Client) {
1911
- res.writeHead(500, { "Content-Type": "text/plain" });
1912
- res.end("Authentication flow not properly initiated.");
1913
- return;
1914
- }
1915
- try {
1916
- const { tokens } = await this.flowOAuth2Client.getToken({
1917
- code,
1918
- codeVerifier: this.codeVerifier || void 0
1919
- });
1920
- await this.tokenManager.saveTokens(tokens);
1921
- this.authCompletedSuccessfully = true;
1922
- this.codeVerifier = null;
1923
- this.codeChallenge = null;
1924
- this.expectedState = null;
1925
- setTimeout(() => {
1926
- this.stop().catch((err) => log("Auth server shutdown error:", err));
1927
- }, 2e3);
1928
- const tokenPath = this.tokenManager.getTokenPath();
1929
- const homeConfig = path3.join(os2.homedir(), ".config");
1930
- const isProjectLevel = !tokenPath.startsWith(homeConfig);
1931
- const credentialsDir = path3.basename(path3.dirname(tokenPath));
1932
- const gitignoreWarning = isProjectLevel ? `
1933
- <div class="warning">
1934
- <p><strong>\u26A0\uFE0F Security:</strong> Add your credentials directory to .gitignore:</p>
1935
- <p><code>${escapeHtml(credentialsDir)}/</code></p>
1936
- </div>` : "";
1937
- res.writeHead(200, { "Content-Type": "text/html" });
1938
- res.end(`
1939
- <!DOCTYPE html>
1940
- <html lang="en">
1941
- <head>
1942
- <meta charset="UTF-8">
1943
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1944
- <title>Authentication Successful</title>
1945
- <style>
1946
- body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; margin: 0; }
1947
- .container { text-align: center; padding: 2em; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 500px; }
1948
- h1 { color: #4CAF50; }
1949
- p { color: #333; margin-bottom: 0.5em; }
1950
- code { background-color: #eee; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
1951
- .warning { background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 1em; margin-top: 1em; }
1952
- .warning p { color: #856404; margin: 0.3em 0; }
1953
- </style>
1954
- </head>
1955
- <body>
1956
- <div class="container">
1957
- <h1>Authentication Successful!</h1>
1958
- <p>Your authentication tokens have been saved successfully to:</p>
1959
- <p><code>${escapeHtml(tokenPath)}</code></p>${gitignoreWarning}
1960
- <p style="margin-top: 1em;">You can now close this browser window.</p>
1961
- </div>
1962
- </body>
1963
- </html>
1964
- `);
1965
- } catch (error) {
1966
- this.authCompletedSuccessfully = false;
1967
- this.codeVerifier = null;
1968
- this.codeChallenge = null;
1969
- this.expectedState = null;
1970
- const authError = mapGoogleError(error);
1971
- log("OAuth callback error:", authError.toToolResponse());
1972
- const fixStepsHtml = authError.fix.map((step, i) => `<li>${i + 1}. ${escapeHtml(step)}</li>`).join("");
1973
- const linksHtml = authError.links ? authError.links.map(
1974
- (link) => `<li><a href="${escapeHtml(link.url)}" target="_blank">${escapeHtml(link.label)}</a></li>`
1975
- ).join("") : "";
1976
- res.writeHead(500, { "Content-Type": "text/html" });
1977
- res.end(`
1978
- <!DOCTYPE html>
1979
- <html lang="en">
1980
- <head>
1981
- <meta charset="UTF-8">
1982
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1983
- <title>Authentication Failed</title>
1984
- <style>
1985
- body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f4f4f4; margin: 0; padding: 1em; }
1986
- .container { text-align: left; padding: 2em; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 600px; }
1987
- h1 { color: #F44336; text-align: center; }
1988
- .error-code { background-color: #ffebee; color: #c62828; padding: 0.5em 1em; border-radius: 4px; font-family: monospace; margin: 1em 0; }
1989
- .reason { color: #333; margin-bottom: 1.5em; }
1990
- h2 { color: #1976d2; font-size: 1.1em; margin-top: 1.5em; }
1991
- ol { padding-left: 1.5em; }
1992
- li { margin-bottom: 0.5em; color: #555; }
1993
- ul { padding-left: 1em; list-style: none; }
1994
- ul li { margin-bottom: 0.3em; }
1995
- a { color: #1976d2; }
1996
- </style>
1997
- </head>
1998
- <body>
1999
- <div class="container">
2000
- <h1>Authentication Failed</h1>
2001
- <div class="error-code">${escapeHtml(authError.code)}</div>
2002
- <p class="reason">${escapeHtml(authError.reason)}</p>
2003
- <h2>How to fix:</h2>
2004
- <ol>${fixStepsHtml}</ol>
2005
- ${linksHtml ? `<h2>Helpful links:</h2><ul>${linksHtml}</ul>` : ""}
2006
- </div>
2007
- </body>
2008
- </html>
2009
- `);
2010
- }
2130
+ await this.handleOAuthCallback(url, res);
2011
2131
  } else {
2012
2132
  res.writeHead(404, { "Content-Type": "text/plain" });
2013
2133
  res.end("Not Found");
@@ -2042,6 +2162,23 @@ var AuthServer = class {
2042
2162
  return false;
2043
2163
  }
2044
2164
  if (openBrowser) {
2165
+ if (this.isClientInvalidError()) {
2166
+ const lastError = getLastTokenAuthError();
2167
+ console.error("\n\u274C AUTHENTICATION BLOCKED");
2168
+ console.error("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2169
+ console.error(`
2170
+ Error: ${lastError?.reason}`);
2171
+ console.error("\nHow to fix:");
2172
+ lastError?.fix.forEach((step, i) => console.error(` ${i + 1}. ${step}`));
2173
+ if (lastError?.links && lastError.links.length > 0) {
2174
+ console.error("\nHelpful links:");
2175
+ lastError.links.forEach((link) => console.error(` - ${link.label}: ${link.url}`));
2176
+ }
2177
+ console.error("");
2178
+ this.authCompletedSuccessfully = false;
2179
+ await this.stop();
2180
+ return false;
2181
+ }
2045
2182
  if (!this.codeChallenge || !this.expectedState) {
2046
2183
  throw new Error("PKCE not initialized - internal error");
2047
2184
  }
@@ -12912,21 +13049,21 @@ function ensureDriveService() {
12912
13049
  hasAccessToken: !!authClient.credentials?.access_token
12913
13050
  });
12914
13051
  }
12915
- var lastAuthError2 = null;
13052
+ var lastAuthError = null;
12916
13053
  async function verifyAuthHealth() {
12917
13054
  if (!drive) {
12918
- lastAuthError2 = "Drive service not initialized";
13055
+ lastAuthError = "Drive service not initialized";
12919
13056
  return false;
12920
13057
  }
12921
13058
  try {
12922
13059
  const response = await drive.about.get({ fields: "user" });
12923
13060
  log("Auth verification successful, user:", response.data.user?.emailAddress);
12924
- lastAuthError2 = null;
13061
+ lastAuthError = null;
12925
13062
  return true;
12926
13063
  } catch (error) {
12927
13064
  const err = error;
12928
- lastAuthError2 = err.message || String(error);
12929
- log("WARNING: Auth verification failed:", lastAuthError2);
13065
+ lastAuthError = err.message || String(error);
13066
+ log("WARNING: Auth verification failed:", lastAuthError);
12930
13067
  if (err.response) {
12931
13068
  log("Auth error details:", {
12932
13069
  status: err.response.status,
@@ -12937,7 +13074,7 @@ async function verifyAuthHealth() {
12937
13074
  }
12938
13075
  }
12939
13076
  function getLastAuthError() {
12940
- return lastAuthError2;
13077
+ return lastAuthError;
12941
13078
  }
12942
13079
  var server = new Server(
12943
13080
  {