@disruptorganic/mcp-google-search-console 1.1.0 → 2.0.0
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/.env.example +70 -112
- package/README.md +0 -0
- package/dist/auth/account-lock.d.ts +26 -0
- package/dist/auth/account-lock.d.ts.map +1 -0
- package/dist/auth/account-lock.js +72 -0
- package/dist/auth/account-lock.js.map +1 -0
- package/dist/auth/token-validator.d.ts +18 -0
- package/dist/auth/token-validator.d.ts.map +1 -0
- package/dist/auth/token-validator.js +175 -0
- package/dist/auth/token-validator.js.map +1 -0
- package/dist/config/index.d.ts +14 -27
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +86 -57
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +119 -106
- package/dist/index.js.map +1 -1
- package/dist/server/http.d.ts +36 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +394 -0
- package/dist/server/http.js.map +1 -0
- package/dist/tools/compare-date-ranges.d.ts +1 -1
- package/dist/tools/compare-date-ranges.d.ts.map +1 -1
- package/dist/tools/compare-date-ranges.js +14 -1
- package/dist/tools/compare-date-ranges.js.map +1 -1
- package/dist/tools/get-property-info.d.ts +1 -1
- package/dist/tools/get-property-info.d.ts.map +1 -1
- package/dist/tools/get-property-info.js +14 -1
- package/dist/tools/get-property-info.js.map +1 -1
- package/dist/tools/get-top-pages.d.ts +1 -1
- package/dist/tools/get-top-pages.d.ts.map +1 -1
- package/dist/tools/get-top-pages.js +14 -1
- package/dist/tools/get-top-pages.js.map +1 -1
- package/dist/tools/get-top-queries.d.ts +1 -1
- package/dist/tools/get-top-queries.d.ts.map +1 -1
- package/dist/tools/get-top-queries.js +14 -1
- package/dist/tools/get-top-queries.js.map +1 -1
- package/dist/tools/health-check.d.ts +1 -1
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +24 -19
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/index.d.ts +2 -77
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -10
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/list-properties.d.ts +1 -1
- package/dist/tools/list-properties.d.ts.map +1 -1
- package/dist/tools/list-properties.js +14 -1
- package/dist/tools/list-properties.js.map +1 -1
- package/dist/tools/query-advanced.d.ts +1 -1
- package/dist/tools/query-advanced.d.ts.map +1 -1
- package/dist/tools/query-advanced.js +14 -1
- package/dist/tools/query-advanced.js.map +1 -1
- package/dist/tools/query-by-keyword.d.ts +1 -1
- package/dist/tools/query-by-keyword.d.ts.map +1 -1
- package/dist/tools/query-by-keyword.js +14 -1
- package/dist/tools/query-by-keyword.js.map +1 -1
- package/dist/tools/query-by-url.d.ts +1 -1
- package/dist/tools/query-by-url.d.ts.map +1 -1
- package/dist/tools/query-by-url.js +14 -1
- package/dist/tools/query-by-url.js.map +1 -1
- package/package.json +18 -7
- package/dist/tools/auth.d.ts +0 -95
- package/dist/tools/auth.d.ts.map +0 -1
- package/dist/tools/auth.js +0 -317
- package/dist/tools/auth.js.map +0 -1
package/.env.example
CHANGED
|
@@ -1,115 +1,73 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Google
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
# OPTIONAL: Token Storage Configuration
|
|
20
|
-
# =============================================================================
|
|
21
|
-
#
|
|
22
|
-
# Path where OAuth2 access and refresh tokens are stored.
|
|
23
|
-
# The directory will be created automatically with secure permissions (0700).
|
|
24
|
-
#
|
|
25
|
-
# Default: ~/.mcp-gsc/tokens.json
|
|
26
|
-
#
|
|
27
|
-
# You can customize this to store tokens in a different location:
|
|
28
|
-
# GSC_TOKEN_PATH=/path/to/your/tokens.json
|
|
29
|
-
#
|
|
30
|
-
# Leave commented to use the default location.
|
|
31
|
-
|
|
32
|
-
# GSC_TOKEN_PATH=~/.mcp-gsc/tokens.json
|
|
33
|
-
|
|
34
|
-
# =============================================================================
|
|
35
|
-
# OPTIONAL: OAuth Redirect URI
|
|
36
|
-
# =============================================================================
|
|
37
|
-
#
|
|
38
|
-
# The OAuth2 redirect URI for the authentication flow.
|
|
39
|
-
#
|
|
40
|
-
# Default: urn:ietf:wg:oauth:2.0:oob
|
|
41
|
-
#
|
|
42
|
-
# The default value uses the "out-of-band" flow, which is appropriate for
|
|
43
|
-
# command-line applications. With this flow, Google displays the authorization
|
|
44
|
-
# code in the browser, and you manually paste it into the terminal.
|
|
45
|
-
#
|
|
46
|
-
# Alternative options:
|
|
47
|
-
# - http://localhost:3000/callback (for local web server flow)
|
|
48
|
-
# - Any custom redirect URI you've configured in Google Cloud Console
|
|
49
|
-
#
|
|
50
|
-
# Note: If you change this, you must also add the same URI to your OAuth2
|
|
51
|
-
# client's "Authorized redirect URIs" in Google Cloud Console.
|
|
52
|
-
#
|
|
53
|
-
# Leave commented to use the default out-of-band flow.
|
|
54
|
-
|
|
55
|
-
# GSC_REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
|
|
56
|
-
|
|
57
|
-
# =============================================================================
|
|
58
|
-
# OPTIONAL: Logging Configuration
|
|
59
|
-
# =============================================================================
|
|
60
|
-
#
|
|
61
|
-
# Set the logging verbosity level for the MCP server.
|
|
62
|
-
#
|
|
63
|
-
# Default: info
|
|
64
|
-
#
|
|
65
|
-
# Available levels:
|
|
66
|
-
# - error: Only critical errors that prevent operation
|
|
67
|
-
# - warn: Warnings and errors (potential issues)
|
|
68
|
-
# - info: General information, warnings, and errors (recommended)
|
|
69
|
-
# - debug: Verbose debugging information (for troubleshooting)
|
|
70
|
-
#
|
|
71
|
-
# Leave commented to use the default 'info' level.
|
|
1
|
+
# Google OAuth 2.0 Configuration
|
|
2
|
+
# Get these from Google Cloud Console: https://console.cloud.google.com
|
|
3
|
+
# See docs/GOOGLE_OAUTH_SETUP.md for detailed setup instructions
|
|
4
|
+
|
|
5
|
+
# REQUIRED: OAuth 2.0 Client ID
|
|
6
|
+
# Example: 123456789-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com
|
|
7
|
+
GOOGLE_CLIENT_ID=
|
|
8
|
+
|
|
9
|
+
# REQUIRED: OAuth 2.0 Client Secret
|
|
10
|
+
# Example: GOCSPX-abcdefghijklmnopqrstuvwxyz
|
|
11
|
+
GOOGLE_CLIENT_SECRET=
|
|
12
|
+
|
|
13
|
+
# OPTIONAL: OAuth 2.0 Redirect URI
|
|
14
|
+
# Default: http://localhost:3456/oauth/callback
|
|
15
|
+
# Only change if Claude Code uses a different callback URI
|
|
16
|
+
# GOOGLE_REDIRECT_URI=http://localhost:3456/oauth/callback
|
|
17
|
+
|
|
18
|
+
# MCP Server Configuration
|
|
72
19
|
|
|
20
|
+
# OPTIONAL: Server port
|
|
21
|
+
# Default: 3456
|
|
22
|
+
# Change if port 3456 is already in use
|
|
23
|
+
# MCP_PORT=3456
|
|
24
|
+
|
|
25
|
+
# OPTIONAL: Server host
|
|
26
|
+
# Default: localhost (0.0.0.0 binds to all interfaces)
|
|
27
|
+
# For security, keep as localhost unless deploying remotely
|
|
28
|
+
# MCP_HOST=localhost
|
|
29
|
+
|
|
30
|
+
# Logging Configuration
|
|
31
|
+
|
|
32
|
+
# OPTIONAL: Log level
|
|
33
|
+
# Options: debug, info, warn, error
|
|
34
|
+
# Default: info
|
|
35
|
+
# Use 'debug' for detailed troubleshooting
|
|
73
36
|
# LOG_LEVEL=info
|
|
74
37
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# For more information, see:
|
|
113
|
-
# - README.md
|
|
114
|
-
# - docs/USER_GUIDE.md
|
|
115
|
-
# - https://developers.google.com/webmaster-tools/v1/how-tos/authorizing
|
|
38
|
+
# Token Storage (Claude Code manages tokens, but keeping for reference)
|
|
39
|
+
|
|
40
|
+
# OPTIONAL: Token storage path
|
|
41
|
+
# Default: ~/.config/gsc-mcp/tokens.json
|
|
42
|
+
# Note: In v2.0.0, Claude Code manages tokens, not the server
|
|
43
|
+
# This is here for backward compatibility only
|
|
44
|
+
# GSC_TOKEN_PATH=~/.config/gsc-mcp/tokens.json
|
|
45
|
+
|
|
46
|
+
# Advanced Configuration
|
|
47
|
+
|
|
48
|
+
# OPTIONAL: Rate limiter - Queries per minute
|
|
49
|
+
# Default: 1200 (Google Search Console API limit)
|
|
50
|
+
# Don't change unless you have a quota increase
|
|
51
|
+
# GSC_RATE_LIMIT_QPM=1200
|
|
52
|
+
|
|
53
|
+
# OPTIONAL: Token validation cache TTL (seconds)
|
|
54
|
+
# Default: 300 (5 minutes)
|
|
55
|
+
# How long to cache Google token validations
|
|
56
|
+
# TOKEN_VALIDATION_CACHE_TTL=300
|
|
57
|
+
|
|
58
|
+
# OPTIONAL: Property list cache TTL (seconds)
|
|
59
|
+
# Default: 86400 (24 hours)
|
|
60
|
+
# How long to cache GSC property list
|
|
61
|
+
# PROPERTY_CACHE_TTL=86400
|
|
62
|
+
|
|
63
|
+
# Development Configuration (not for production)
|
|
64
|
+
|
|
65
|
+
# OPTIONAL: Environment
|
|
66
|
+
# Default: production
|
|
67
|
+
# Set to 'development' for more verbose logging
|
|
68
|
+
# NODE_ENV=production
|
|
69
|
+
|
|
70
|
+
# OPTIONAL: Enable debug mode
|
|
71
|
+
# Default: false
|
|
72
|
+
# Set to 'true' for maximum verbosity
|
|
73
|
+
# DEBUG=false
|
package/README.md
CHANGED
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface LockedUser {
|
|
2
|
+
userId: string;
|
|
3
|
+
email: string;
|
|
4
|
+
lockedAt: Date;
|
|
5
|
+
}
|
|
6
|
+
export interface LockCheckResult {
|
|
7
|
+
allowed: boolean;
|
|
8
|
+
lockedUser?: LockedUser;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function isLocked(): boolean;
|
|
12
|
+
export declare function getLockedUser(): LockedUser | null;
|
|
13
|
+
export declare function checkUserAccess(userId: string, email: string): LockCheckResult;
|
|
14
|
+
export declare function getLockStatus(): {
|
|
15
|
+
locked: boolean;
|
|
16
|
+
account: null;
|
|
17
|
+
since: null;
|
|
18
|
+
userId?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
locked: boolean;
|
|
21
|
+
account: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
since: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function clearLock(): void;
|
|
26
|
+
//# sourceMappingURL=account-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-lock.d.ts","sourceRoot":"","sources":["../../src/auth/account-lock.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,IAAI,CAAC;CAChB;AAKD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AAKD,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AASD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CA+C9E;AAKD,wBAAgB,aAAa;;;;;;;;;;EAe5B;AAMD,wBAAgB,SAAS,IAAI,IAAI,CAQhC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
let lockedUser = null;
|
|
3
|
+
export function isLocked() {
|
|
4
|
+
return lockedUser !== null;
|
|
5
|
+
}
|
|
6
|
+
export function getLockedUser() {
|
|
7
|
+
return lockedUser;
|
|
8
|
+
}
|
|
9
|
+
export function checkUserAccess(userId, email) {
|
|
10
|
+
if (lockedUser === null) {
|
|
11
|
+
lockedUser = {
|
|
12
|
+
userId,
|
|
13
|
+
email,
|
|
14
|
+
lockedAt: new Date(),
|
|
15
|
+
};
|
|
16
|
+
logger.info('Server locked to user account', {
|
|
17
|
+
userId,
|
|
18
|
+
email,
|
|
19
|
+
lockedAt: lockedUser.lockedAt.toISOString(),
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
allowed: true,
|
|
23
|
+
lockedUser,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (lockedUser.userId === userId) {
|
|
27
|
+
logger.debug('Access allowed for locked user', {
|
|
28
|
+
userId,
|
|
29
|
+
email,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
allowed: true,
|
|
33
|
+
lockedUser,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
logger.warn('Access denied for different user', {
|
|
37
|
+
requestingUserId: userId,
|
|
38
|
+
requestingEmail: email,
|
|
39
|
+
lockedUserId: lockedUser.userId,
|
|
40
|
+
lockedEmail: lockedUser.email,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
allowed: false,
|
|
44
|
+
lockedUser,
|
|
45
|
+
reason: `Server is locked to account ${lockedUser.email}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function getLockStatus() {
|
|
49
|
+
if (lockedUser === null) {
|
|
50
|
+
return {
|
|
51
|
+
locked: false,
|
|
52
|
+
account: null,
|
|
53
|
+
since: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
locked: true,
|
|
58
|
+
account: lockedUser.email,
|
|
59
|
+
userId: lockedUser.userId,
|
|
60
|
+
since: lockedUser.lockedAt.toISOString(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function clearLock() {
|
|
64
|
+
if (lockedUser) {
|
|
65
|
+
logger.info('Lock cleared', {
|
|
66
|
+
previousUser: lockedUser.email,
|
|
67
|
+
previousUserId: lockedUser.userId,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
lockedUser = null;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=account-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-lock.js","sourceRoot":"","sources":["../../src/auth/account-lock.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAwB5C,IAAI,UAAU,GAAsB,IAAI,CAAC;AAKzC,MAAM,UAAU,QAAQ;IACtB,OAAO,UAAU,KAAK,IAAI,CAAC;AAC7B,CAAC;AAKD,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC;AACpB,CAAC;AASD,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,KAAa;IAE3D,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,UAAU,GAAG;YACX,MAAM;YACN,KAAK;YACL,QAAQ,EAAE,IAAI,IAAI,EAAE;SACrB,CAAC;QAEF,MAAM,CAAC,IAAI,CAAC,+BAA+B,EAAE;YAC3C,MAAM;YACN,KAAK;YACL,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,WAAW,EAAE;SAC5C,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE,IAAI;YACb,UAAU;SACX,CAAC;IACJ,CAAC;IAGD,IAAI,UAAU,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,gCAAgC,EAAE;YAC7C,MAAM;YACN,KAAK;SACN,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE,IAAI;YACb,UAAU;SACX,CAAC;IACJ,CAAC;IAGD,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE;QAC9C,gBAAgB,EAAE,MAAM;QACxB,eAAe,EAAE,KAAK;QACtB,YAAY,EAAE,UAAU,CAAC,MAAM;QAC/B,WAAW,EAAE,UAAU,CAAC,KAAK;KAC9B,CAAC,CAAC;IAEH,OAAO;QACL,OAAO,EAAE,KAAK;QACd,UAAU;QACV,MAAM,EAAE,+BAA+B,UAAU,CAAC,KAAK,EAAE;KAC1D,CAAC;AACJ,CAAC;AAKD,MAAM,UAAU,aAAa;IAC3B,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO;YACL,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,IAAI;SACZ,CAAC;IACJ,CAAC;IAED,OAAO;QACL,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,UAAU,CAAC,KAAK;QACzB,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,WAAW,EAAE;KACzC,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,SAAS;IACvB,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE;YAC1B,YAAY,EAAE,UAAU,CAAC,KAAK;YAC9B,cAAc,EAAE,UAAU,CAAC,MAAM;SAClC,CAAC,CAAC;IACL,CAAC;IACD,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TokenValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
email?: string;
|
|
4
|
+
userId?: string;
|
|
5
|
+
scope?: string;
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateToken(accessToken: string): Promise<TokenValidationResult>;
|
|
10
|
+
export declare function clearValidationCache(): void;
|
|
11
|
+
export declare function getCacheStats(): {
|
|
12
|
+
keys: number;
|
|
13
|
+
hits: number;
|
|
14
|
+
misses: number;
|
|
15
|
+
hitRate: number;
|
|
16
|
+
ttl: number;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=token-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-validator.d.ts","sourceRoot":"","sources":["../../src/auth/token-validator.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmND,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAgDvF;AAMD,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C;AAMD,wBAAgB,aAAa;;;;;;EAS5B"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
import NodeCache from 'node-cache';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
const CACHE_TTL_SECONDS = 5 * 60;
|
|
5
|
+
const CACHE_CHECK_PERIOD = 60;
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
const BASE_DELAY_MS = 1000;
|
|
8
|
+
const validationCache = new NodeCache({
|
|
9
|
+
stdTTL: CACHE_TTL_SECONDS,
|
|
10
|
+
checkperiod: CACHE_CHECK_PERIOD,
|
|
11
|
+
useClones: false,
|
|
12
|
+
});
|
|
13
|
+
const oauth2Client = new OAuth2Client();
|
|
14
|
+
function redactToken(token) {
|
|
15
|
+
if (!token || token.length <= 8) {
|
|
16
|
+
return '***';
|
|
17
|
+
}
|
|
18
|
+
return `${token.substring(0, 8)}...`;
|
|
19
|
+
}
|
|
20
|
+
function getCacheKey(accessToken) {
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (let i = 0; i < accessToken.length; i++) {
|
|
23
|
+
const char = accessToken.charCodeAt(i);
|
|
24
|
+
hash = ((hash << 5) - hash) + char;
|
|
25
|
+
hash = hash & hash;
|
|
26
|
+
}
|
|
27
|
+
return `token_${Math.abs(hash)}`;
|
|
28
|
+
}
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
async function validateWithGoogle(accessToken) {
|
|
33
|
+
let lastError;
|
|
34
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
logger.debug('Validating token with Google API', {
|
|
37
|
+
attempt: attempt + 1,
|
|
38
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
39
|
+
token: redactToken(accessToken),
|
|
40
|
+
});
|
|
41
|
+
const tokenInfo = await oauth2Client.getTokenInfo(accessToken);
|
|
42
|
+
logger.debug('Token validation successful', {
|
|
43
|
+
email: tokenInfo.email,
|
|
44
|
+
userId: tokenInfo.sub,
|
|
45
|
+
hasScope: !!tokenInfo.scope,
|
|
46
|
+
expiresIn: tokenInfo.expires_in,
|
|
47
|
+
});
|
|
48
|
+
if (tokenInfo.expires_in !== undefined && tokenInfo.expires_in <= 0) {
|
|
49
|
+
logger.warn('Token is expired', {
|
|
50
|
+
expiresIn: tokenInfo.expires_in,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
error: 'Token expired',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
valid: true,
|
|
59
|
+
email: tokenInfo.email,
|
|
60
|
+
userId: tokenInfo.sub,
|
|
61
|
+
scope: tokenInfo.scope,
|
|
62
|
+
accessToken,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
lastError = error;
|
|
67
|
+
const errorMessage = error.message || String(error);
|
|
68
|
+
const statusCode = error.response?.status || error.code;
|
|
69
|
+
logger.debug('Token validation error', {
|
|
70
|
+
attempt: attempt + 1,
|
|
71
|
+
error: errorMessage,
|
|
72
|
+
statusCode,
|
|
73
|
+
});
|
|
74
|
+
if (errorMessage.includes('invalid_token') ||
|
|
75
|
+
errorMessage.includes('Invalid Value') ||
|
|
76
|
+
statusCode === 400) {
|
|
77
|
+
logger.warn('Invalid token', {
|
|
78
|
+
error: errorMessage,
|
|
79
|
+
token: redactToken(accessToken),
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
error: 'Invalid token',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (statusCode === 429) {
|
|
87
|
+
const retryAfter = error.response?.headers?.['retry-after'];
|
|
88
|
+
logger.warn('Rate limited by Google API', {
|
|
89
|
+
retryAfter,
|
|
90
|
+
attempt: attempt + 1,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
valid: false,
|
|
94
|
+
error: retryAfter
|
|
95
|
+
? `Rate limited. Retry after ${retryAfter} seconds.`
|
|
96
|
+
: 'Rate limited by Google API. Please try again later.',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (attempt < MAX_RETRIES) {
|
|
100
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
101
|
+
logger.debug('Retrying token validation', {
|
|
102
|
+
attempt: attempt + 1,
|
|
103
|
+
delayMs: delay,
|
|
104
|
+
error: errorMessage,
|
|
105
|
+
});
|
|
106
|
+
await sleep(delay);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
logger.error('Token validation failed after retries', {
|
|
110
|
+
attempts: MAX_RETRIES + 1,
|
|
111
|
+
error: errorMessage,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
valid: false,
|
|
115
|
+
error: `Token validation failed: ${errorMessage}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
valid: false,
|
|
121
|
+
error: `Token validation failed: ${lastError?.message || 'Unknown error'}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export async function validateToken(accessToken) {
|
|
125
|
+
if (!accessToken || typeof accessToken !== 'string' || !accessToken.trim()) {
|
|
126
|
+
logger.debug('Empty or invalid token provided');
|
|
127
|
+
return {
|
|
128
|
+
valid: false,
|
|
129
|
+
error: 'Missing or invalid token',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const token = accessToken.trim();
|
|
133
|
+
const cacheKey = getCacheKey(token);
|
|
134
|
+
const cached = validationCache.get(cacheKey);
|
|
135
|
+
if (cached) {
|
|
136
|
+
logger.debug('Token validation cache hit', {
|
|
137
|
+
valid: cached.valid,
|
|
138
|
+
email: cached.email,
|
|
139
|
+
userId: cached.userId,
|
|
140
|
+
token: redactToken(token),
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
...cached,
|
|
144
|
+
accessToken: token,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
logger.debug('Token validation cache miss', {
|
|
148
|
+
token: redactToken(token),
|
|
149
|
+
});
|
|
150
|
+
const result = await validateWithGoogle(token);
|
|
151
|
+
validationCache.set(cacheKey, result);
|
|
152
|
+
logger.info('Token validated and cached', {
|
|
153
|
+
valid: result.valid,
|
|
154
|
+
email: result.email,
|
|
155
|
+
userId: result.userId,
|
|
156
|
+
error: result.error,
|
|
157
|
+
token: redactToken(token),
|
|
158
|
+
});
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
export function clearValidationCache() {
|
|
162
|
+
validationCache.flushAll();
|
|
163
|
+
logger.info('Token validation cache cleared');
|
|
164
|
+
}
|
|
165
|
+
export function getCacheStats() {
|
|
166
|
+
const stats = validationCache.getStats();
|
|
167
|
+
return {
|
|
168
|
+
keys: validationCache.keys().length,
|
|
169
|
+
hits: stats.hits,
|
|
170
|
+
misses: stats.misses,
|
|
171
|
+
hitRate: stats.hits / (stats.hits + stats.misses) || 0,
|
|
172
|
+
ttl: CACHE_TTL_SECONDS,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=token-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-validator.js","sourceRoot":"","sources":["../../src/auth/token-validator.ts"],"names":[],"mappings":"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"}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,36 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
declare const configSchema: z.ZodObject<{
|
|
3
|
-
clientId: z.ZodString;
|
|
4
|
-
clientSecret: z.ZodString;
|
|
5
|
-
tokenPath: z.ZodDefault<z.ZodString>;
|
|
6
|
-
redirectUri: z.ZodDefault<z.ZodString>;
|
|
7
|
-
logLevel: z.ZodDefault<z.ZodEnum<["error", "warn", "info", "debug"]>>;
|
|
8
|
-
}, "strip", z.ZodTypeAny, {
|
|
1
|
+
export interface AppConfig {
|
|
9
2
|
clientId: string;
|
|
10
3
|
clientSecret: string;
|
|
11
|
-
tokenPath: string;
|
|
12
4
|
redirectUri: string;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
clientSecret: string;
|
|
17
|
-
tokenPath?: string | undefined;
|
|
18
|
-
redirectUri?: string | undefined;
|
|
19
|
-
logLevel?: "info" | "error" | "warn" | "debug" | undefined;
|
|
20
|
-
}>;
|
|
21
|
-
export type Config = z.infer<typeof configSchema>;
|
|
22
|
-
export declare const config: {
|
|
23
|
-
clientId: string;
|
|
24
|
-
clientSecret: string;
|
|
5
|
+
port: number;
|
|
6
|
+
host: string;
|
|
7
|
+
logLevel: string;
|
|
25
8
|
tokenPath: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
rateLimitQPM: number;
|
|
10
|
+
tokenValidationCacheTTL: number;
|
|
11
|
+
propertyCacheTTL: number;
|
|
12
|
+
nodeEnv: string;
|
|
13
|
+
isDevelopment: boolean;
|
|
14
|
+
isProduction: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const config: AppConfig;
|
|
17
|
+
export declare function getEnvironment(): string;
|
|
29
18
|
export declare function getTokenDirectory(): string;
|
|
30
19
|
export declare function isDevelopment(): boolean;
|
|
31
20
|
export declare function isProduction(): boolean;
|
|
32
|
-
export declare function
|
|
21
|
+
export declare function logConfiguration(): void;
|
|
33
22
|
export declare function getConfigSummary(): Record<string, string>;
|
|
34
|
-
export declare function logConfigSummary(): void;
|
|
35
|
-
export {};
|
|
36
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,SAAS;IAExB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IAGb,QAAQ,EAAE,MAAM,CAAC;IAGjB,SAAS,EAAE,MAAM,CAAC;IAGlB,YAAY,EAAE,MAAM,CAAC;IAGrB,uBAAuB,EAAE,MAAM,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IAGzB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;CACvB;AA8GD,eAAO,MAAM,MAAM,EAAE,SAAwB,CAAC;AAS9C,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAKD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAKD,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAKD,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AASD,wBAAgB,gBAAgB,IAAI,IAAI,CAsBvC;AAMD,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAczD"}
|