@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 +332 -195
- package/dist/index.js.map +4 -4
- package/package.json +2 -1
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
|
|
1381
|
-
const
|
|
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
|
-
|
|
1488
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
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
|
|
1622
|
+
var activeTokenManager = null;
|
|
1597
1623
|
function getLastTokenAuthError() {
|
|
1598
|
-
return
|
|
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",
|
|
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.
|
|
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/
|
|
1882
|
+
// src/auth/templates.ts
|
|
1841
1883
|
function escapeHtml(str) {
|
|
1842
1884
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13052
|
+
var lastAuthError = null;
|
|
12916
13053
|
async function verifyAuthHealth() {
|
|
12917
13054
|
if (!drive) {
|
|
12918
|
-
|
|
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
|
-
|
|
13061
|
+
lastAuthError = null;
|
|
12925
13062
|
return true;
|
|
12926
13063
|
} catch (error) {
|
|
12927
13064
|
const err = error;
|
|
12928
|
-
|
|
12929
|
-
log("WARNING: Auth verification failed:",
|
|
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
|
|
13077
|
+
return lastAuthError;
|
|
12941
13078
|
}
|
|
12942
13079
|
var server = new Server(
|
|
12943
13080
|
{
|