@juspay/neurolink 9.14.0 → 9.16.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/CHANGELOG.md +12 -0
- package/README.md +15 -15
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/adapters/video/videoAnalyzer.js +10 -8
- package/dist/auth/anthropicOAuth.d.ts +377 -0
- package/dist/auth/anthropicOAuth.js +914 -0
- package/dist/auth/index.d.ts +20 -0
- package/dist/auth/index.js +29 -0
- package/dist/auth/tokenStore.d.ts +225 -0
- package/dist/auth/tokenStore.js +521 -0
- package/dist/cli/commands/auth.d.ts +50 -0
- package/dist/cli/commands/auth.js +1115 -0
- package/dist/cli/commands/setup-anthropic.js +1 -14
- package/dist/cli/commands/setup-azure.js +1 -12
- package/dist/cli/commands/setup-bedrock.js +1 -9
- package/dist/cli/commands/setup-google-ai.js +1 -12
- package/dist/cli/commands/setup-openai.js +1 -14
- package/dist/cli/commands/workflow.d.ts +27 -0
- package/dist/cli/commands/workflow.js +216 -0
- package/dist/cli/factories/authCommandFactory.d.ts +52 -0
- package/dist/cli/factories/authCommandFactory.js +146 -0
- package/dist/cli/factories/commandFactory.d.ts +6 -0
- package/dist/cli/factories/commandFactory.js +171 -22
- package/dist/cli/index.js +0 -1
- package/dist/cli/parser.js +14 -2
- package/dist/cli/utils/maskCredential.d.ts +11 -0
- package/dist/cli/utils/maskCredential.js +23 -0
- package/dist/constants/contextWindows.js +107 -16
- package/dist/constants/enums.d.ts +119 -15
- package/dist/constants/enums.js +182 -22
- package/dist/constants/index.d.ts +3 -1
- package/dist/constants/index.js +11 -1
- package/dist/context/budgetChecker.js +1 -1
- package/dist/context/contextCompactor.js +31 -4
- package/dist/context/emergencyTruncation.d.ts +21 -0
- package/dist/context/emergencyTruncation.js +88 -0
- package/dist/context/errorDetection.d.ts +16 -0
- package/dist/context/errorDetection.js +48 -1
- package/dist/context/errors.d.ts +19 -0
- package/dist/context/errors.js +21 -0
- package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/core/baseProvider.js +306 -200
- package/dist/core/conversationMemoryManager.js +104 -61
- package/dist/core/evaluationProviders.js +16 -33
- package/dist/core/factory.js +237 -164
- package/dist/core/modules/GenerationHandler.js +175 -116
- package/dist/core/modules/MessageBuilder.js +222 -170
- package/dist/core/modules/StreamHandler.d.ts +1 -0
- package/dist/core/modules/StreamHandler.js +95 -27
- package/dist/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/core/modules/TelemetryHandler.js +25 -7
- package/dist/core/modules/ToolsManager.js +115 -191
- package/dist/core/redisConversationMemoryManager.js +418 -282
- package/dist/factories/providerRegistry.d.ts +5 -0
- package/dist/factories/providerRegistry.js +20 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -2
- package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
- package/dist/lib/auth/anthropicOAuth.d.ts +377 -0
- package/dist/lib/auth/anthropicOAuth.js +915 -0
- package/dist/lib/auth/index.d.ts +20 -0
- package/dist/lib/auth/index.js +30 -0
- package/dist/lib/auth/tokenStore.d.ts +225 -0
- package/dist/lib/auth/tokenStore.js +522 -0
- package/dist/lib/constants/contextWindows.js +107 -16
- package/dist/lib/constants/enums.d.ts +119 -15
- package/dist/lib/constants/enums.js +182 -22
- package/dist/lib/constants/index.d.ts +3 -1
- package/dist/lib/constants/index.js +11 -1
- package/dist/lib/context/budgetChecker.js +1 -1
- package/dist/lib/context/contextCompactor.js +31 -4
- package/dist/lib/context/emergencyTruncation.d.ts +21 -0
- package/dist/lib/context/emergencyTruncation.js +89 -0
- package/dist/lib/context/errorDetection.d.ts +16 -0
- package/dist/lib/context/errorDetection.js +48 -1
- package/dist/lib/context/errors.d.ts +19 -0
- package/dist/lib/context/errors.js +22 -0
- package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/lib/core/baseProvider.js +306 -200
- package/dist/lib/core/conversationMemoryManager.js +104 -61
- package/dist/lib/core/evaluationProviders.js +16 -33
- package/dist/lib/core/factory.js +237 -164
- package/dist/lib/core/modules/GenerationHandler.js +175 -116
- package/dist/lib/core/modules/MessageBuilder.js +222 -170
- package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
- package/dist/lib/core/modules/StreamHandler.js +95 -27
- package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/lib/core/modules/TelemetryHandler.js +25 -7
- package/dist/lib/core/modules/ToolsManager.js +115 -191
- package/dist/lib/core/redisConversationMemoryManager.js +418 -282
- package/dist/lib/factories/providerRegistry.d.ts +5 -0
- package/dist/lib/factories/providerRegistry.js +20 -2
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.js +4 -2
- package/dist/lib/mcp/externalServerManager.js +66 -0
- package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/lib/mcp/mcpClientFactory.js +16 -0
- package/dist/lib/mcp/toolDiscoveryService.js +32 -6
- package/dist/lib/mcp/toolRegistry.js +193 -123
- package/dist/lib/models/anthropicModels.d.ts +267 -0
- package/dist/lib/models/anthropicModels.js +528 -0
- package/dist/lib/neurolink.d.ts +6 -0
- package/dist/lib/neurolink.js +1162 -646
- package/dist/lib/providers/amazonBedrock.d.ts +1 -1
- package/dist/lib/providers/amazonBedrock.js +521 -319
- package/dist/lib/providers/anthropic.d.ts +123 -2
- package/dist/lib/providers/anthropic.js +873 -27
- package/dist/lib/providers/anthropicBaseProvider.js +77 -17
- package/dist/lib/providers/googleAiStudio.d.ts +1 -1
- package/dist/lib/providers/googleAiStudio.js +292 -227
- package/dist/lib/providers/googleVertex.d.ts +36 -1
- package/dist/lib/providers/googleVertex.js +553 -260
- package/dist/lib/providers/ollama.js +329 -278
- package/dist/lib/providers/openAI.js +77 -19
- package/dist/lib/providers/sagemaker/parsers.js +3 -3
- package/dist/lib/providers/sagemaker/streaming.js +3 -3
- package/dist/lib/proxy/proxyFetch.js +81 -48
- package/dist/lib/rag/ChunkerFactory.js +1 -1
- package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/lib/rag/chunking/markdownChunker.js +174 -2
- package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
- package/dist/lib/rag/ragIntegration.d.ts +18 -1
- package/dist/lib/rag/ragIntegration.js +94 -14
- package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
- package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/lib/telemetry/attributes.d.ts +52 -0
- package/dist/lib/telemetry/attributes.js +61 -0
- package/dist/lib/telemetry/index.d.ts +3 -0
- package/dist/lib/telemetry/index.js +3 -0
- package/dist/lib/telemetry/telemetryService.d.ts +6 -0
- package/dist/lib/telemetry/telemetryService.js +6 -0
- package/dist/lib/telemetry/tracers.d.ts +15 -0
- package/dist/lib/telemetry/tracers.js +17 -0
- package/dist/lib/telemetry/withSpan.d.ts +9 -0
- package/dist/lib/telemetry/withSpan.js +35 -0
- package/dist/lib/types/contextTypes.d.ts +10 -0
- package/dist/lib/types/errors.d.ts +62 -0
- package/dist/lib/types/errors.js +107 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/providers.d.ts +107 -0
- package/dist/lib/types/providers.js +69 -0
- package/dist/lib/types/streamTypes.d.ts +14 -0
- package/dist/lib/types/subscriptionTypes.d.ts +893 -0
- package/dist/lib/types/subscriptionTypes.js +8 -0
- package/dist/lib/utils/conversationMemory.js +121 -82
- package/dist/lib/utils/logger.d.ts +5 -0
- package/dist/lib/utils/logger.js +50 -2
- package/dist/lib/utils/messageBuilder.js +22 -42
- package/dist/lib/utils/modelDetection.js +3 -3
- package/dist/lib/utils/providerConfig.d.ts +167 -0
- package/dist/lib/utils/providerConfig.js +619 -9
- package/dist/lib/utils/providerRetry.d.ts +41 -0
- package/dist/lib/utils/providerRetry.js +114 -0
- package/dist/lib/utils/retryability.d.ts +14 -0
- package/dist/lib/utils/retryability.js +23 -0
- package/dist/lib/utils/sanitizers/svg.js +4 -5
- package/dist/lib/utils/tokenEstimation.d.ts +11 -1
- package/dist/lib/utils/tokenEstimation.js +19 -4
- package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
- package/dist/mcp/externalServerManager.js +66 -0
- package/dist/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/mcp/mcpClientFactory.js +16 -0
- package/dist/mcp/toolDiscoveryService.js +32 -6
- package/dist/mcp/toolRegistry.js +193 -123
- package/dist/models/anthropicModels.d.ts +267 -0
- package/dist/models/anthropicModels.js +527 -0
- package/dist/neurolink.d.ts +6 -0
- package/dist/neurolink.js +1162 -646
- package/dist/providers/amazonBedrock.d.ts +1 -1
- package/dist/providers/amazonBedrock.js +521 -319
- package/dist/providers/anthropic.d.ts +123 -2
- package/dist/providers/anthropic.js +873 -27
- package/dist/providers/anthropicBaseProvider.js +77 -17
- package/dist/providers/googleAiStudio.d.ts +1 -1
- package/dist/providers/googleAiStudio.js +292 -227
- package/dist/providers/googleVertex.d.ts +36 -1
- package/dist/providers/googleVertex.js +553 -260
- package/dist/providers/ollama.js +329 -278
- package/dist/providers/openAI.js +77 -19
- package/dist/providers/sagemaker/parsers.js +3 -3
- package/dist/providers/sagemaker/streaming.js +3 -3
- package/dist/proxy/proxyFetch.js +81 -48
- package/dist/rag/ChunkerFactory.js +1 -1
- package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/rag/chunking/markdownChunker.js +174 -2
- package/dist/rag/pipeline/contextAssembly.js +2 -1
- package/dist/rag/ragIntegration.d.ts +18 -1
- package/dist/rag/ragIntegration.js +94 -14
- package/dist/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/server/abstract/baseServerAdapter.js +4 -1
- package/dist/server/adapters/fastifyAdapter.js +35 -30
- package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/telemetry/attributes.d.ts +52 -0
- package/dist/telemetry/attributes.js +60 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.js +3 -0
- package/dist/telemetry/telemetryService.d.ts +6 -0
- package/dist/telemetry/telemetryService.js +6 -0
- package/dist/telemetry/tracers.d.ts +15 -0
- package/dist/telemetry/tracers.js +16 -0
- package/dist/telemetry/withSpan.d.ts +9 -0
- package/dist/telemetry/withSpan.js +34 -0
- package/dist/types/contextTypes.d.ts +10 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/errors.js +107 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +2 -0
- package/dist/types/providers.d.ts +107 -0
- package/dist/types/providers.js +69 -0
- package/dist/types/streamTypes.d.ts +14 -0
- package/dist/types/subscriptionTypes.d.ts +893 -0
- package/dist/types/subscriptionTypes.js +7 -0
- package/dist/utils/conversationMemory.js +121 -82
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +50 -2
- package/dist/utils/messageBuilder.js +22 -42
- package/dist/utils/modelDetection.js +3 -3
- package/dist/utils/providerConfig.d.ts +167 -0
- package/dist/utils/providerConfig.js +619 -9
- package/dist/utils/providerRetry.d.ts +41 -0
- package/dist/utils/providerRetry.js +113 -0
- package/dist/utils/retryability.d.ts +14 -0
- package/dist/utils/retryability.js +22 -0
- package/dist/utils/sanitizers/svg.js +4 -5
- package/dist/utils/tokenEstimation.d.ts +11 -1
- package/dist/utils/tokenEstimation.js +19 -4
- package/dist/utils/videoAnalysisProcessor.js +7 -3
- package/dist/workflow/config.d.ts +26 -26
- package/package.json +2 -1
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic OAuth 2.0 Authentication for Claude Pro/Max Subscriptions
|
|
3
|
+
*
|
|
4
|
+
* This module implements OAuth 2.0 flow with PKCE support for authenticating
|
|
5
|
+
* Claude Pro and Max subscription users through console.anthropic.com.
|
|
6
|
+
*
|
|
7
|
+
* OAuth Flow:
|
|
8
|
+
* 1. Generate PKCE code verifier and challenge
|
|
9
|
+
* 2. User is redirected to Anthropic authorization URL
|
|
10
|
+
* 3. User authenticates and grants permissions
|
|
11
|
+
* 4. Callback receives authorization code
|
|
12
|
+
* 5. Code is exchanged for access and refresh tokens
|
|
13
|
+
* 6. Tokens are used for API authentication
|
|
14
|
+
*
|
|
15
|
+
* @module auth/anthropicOAuth
|
|
16
|
+
*/
|
|
17
|
+
import { createHash, randomBytes } from "crypto";
|
|
18
|
+
import { createServer, IncomingMessage, ServerResponse } from "http";
|
|
19
|
+
import { OAuthError, OAuthConfigurationError, OAuthTokenExchangeError, OAuthTokenRefreshError, OAuthTokenRevocationError, OAuthCallbackServerError, } from "../types/errors.js";
|
|
20
|
+
import { logger } from "../utils/logger.js";
|
|
21
|
+
/**
|
|
22
|
+
* HTML-escape a string to prevent XSS when embedding in HTML responses.
|
|
23
|
+
*/
|
|
24
|
+
function escapeHtml(str) {
|
|
25
|
+
return str
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, """)
|
|
30
|
+
.replace(/'/g, "'");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Redact likely tokens/secrets from a string before logging.
|
|
34
|
+
* Replaces JWTs and long opaque token strings.
|
|
35
|
+
*/
|
|
36
|
+
function redactTokens(s) {
|
|
37
|
+
return s
|
|
38
|
+
.replace(/[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/g, "[JWT]")
|
|
39
|
+
.replace(/\b[A-Za-z0-9\-_]{32,}\b/g, "[TOKEN]");
|
|
40
|
+
}
|
|
41
|
+
// Re-export error classes for backward compatibility
|
|
42
|
+
export { OAuthError, OAuthConfigurationError, OAuthTokenExchangeError, OAuthTokenRefreshError, OAuthTokenValidationError, OAuthTokenRevocationError, OAuthCallbackServerError, } from "../types/errors.js";
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// OAUTH CONSTANTS (Claude Code Official)
|
|
45
|
+
// =============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Claude Code's official OAuth client ID
|
|
48
|
+
* Used to authenticate with Anthropic's OAuth system
|
|
49
|
+
*/
|
|
50
|
+
export const CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
51
|
+
/**
|
|
52
|
+
* Anthropic OAuth authorization URL for Claude Pro/Max
|
|
53
|
+
*/
|
|
54
|
+
export const ANTHROPIC_AUTH_URL = "https://claude.ai/oauth/authorize";
|
|
55
|
+
/**
|
|
56
|
+
* Anthropic OAuth token endpoint
|
|
57
|
+
*/
|
|
58
|
+
export const ANTHROPIC_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
|
|
59
|
+
/**
|
|
60
|
+
* Anthropic OAuth redirect URI (official callback)
|
|
61
|
+
*/
|
|
62
|
+
export const ANTHROPIC_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
|
|
63
|
+
/**
|
|
64
|
+
* Default OAuth scopes for Claude subscription access
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_SCOPES = [
|
|
67
|
+
"org:create_api_key",
|
|
68
|
+
"user:profile",
|
|
69
|
+
"user:inference",
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* User-Agent string to spoof Claude CLI
|
|
73
|
+
*/
|
|
74
|
+
export const CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.2 (external, cli)";
|
|
75
|
+
/**
|
|
76
|
+
* Required beta headers for OAuth API requests.
|
|
77
|
+
* The "oauth-2025-04-20" header is CRITICAL for OAuth authentication.
|
|
78
|
+
* The "interleaved-thinking-2025-05-14" enables extended thinking.
|
|
79
|
+
*/
|
|
80
|
+
export const OAUTH_BETA_HEADERS = "oauth-2025-04-20,interleaved-thinking-2025-05-14";
|
|
81
|
+
/**
|
|
82
|
+
* Tool name prefix required for OAuth API requests
|
|
83
|
+
*/
|
|
84
|
+
export const MCP_TOOL_PREFIX = "mcp_";
|
|
85
|
+
/**
|
|
86
|
+
* @deprecated Use ANTHROPIC_AUTH_URL instead
|
|
87
|
+
*/
|
|
88
|
+
export const ANTHROPIC_OAUTH_BASE_URL = "https://console.anthropic.com/oauth";
|
|
89
|
+
/**
|
|
90
|
+
* @deprecated Use ANTHROPIC_REDIRECT_URI instead
|
|
91
|
+
*/
|
|
92
|
+
export const DEFAULT_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
|
|
93
|
+
/**
|
|
94
|
+
* Default local callback server port (for local testing only)
|
|
95
|
+
*/
|
|
96
|
+
export const DEFAULT_CALLBACK_PORT = 8787;
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// MAIN OAUTH CLASS
|
|
99
|
+
// =============================================================================
|
|
100
|
+
/**
|
|
101
|
+
* AnthropicOAuth - OAuth 2.0 authentication for Claude Pro/Max subscriptions
|
|
102
|
+
*
|
|
103
|
+
* Implements OAuth 2.0 authorization code flow with PKCE support for
|
|
104
|
+
* authenticating users with Claude Pro or Max subscriptions.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const oauth = new AnthropicOAuth({
|
|
109
|
+
* clientId: "your-client-id",
|
|
110
|
+
* redirectUri: "http://localhost:8787/callback",
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* // Generate PKCE parameters
|
|
114
|
+
* const codeVerifier = AnthropicOAuth.generateCodeVerifier();
|
|
115
|
+
* const codeChallenge = await AnthropicOAuth.generateCodeChallenge(codeVerifier);
|
|
116
|
+
*
|
|
117
|
+
* // Generate auth URL
|
|
118
|
+
* const authUrl = oauth.generateAuthUrl({
|
|
119
|
+
* codeChallenge,
|
|
120
|
+
* state: "random-state",
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* // After user authenticates, exchange code for tokens
|
|
124
|
+
* const tokens = await oauth.exchangeCodeForTokens(code, codeVerifier);
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export class AnthropicOAuth {
|
|
128
|
+
clientId;
|
|
129
|
+
clientSecret;
|
|
130
|
+
redirectUri;
|
|
131
|
+
scopes;
|
|
132
|
+
authorizationUrl;
|
|
133
|
+
tokenUrl;
|
|
134
|
+
validationUrl;
|
|
135
|
+
revocationUrl;
|
|
136
|
+
constructor(config = {}) {
|
|
137
|
+
// Get client ID from config or environment, defaulting to Claude Code's official client ID
|
|
138
|
+
this.clientId =
|
|
139
|
+
config.clientId ||
|
|
140
|
+
process.env.ANTHROPIC_OAUTH_CLIENT_ID ||
|
|
141
|
+
CLAUDE_CODE_CLIENT_ID;
|
|
142
|
+
if (!this.clientId) {
|
|
143
|
+
throw new OAuthConfigurationError("Missing OAuth client ID. Set ANTHROPIC_OAUTH_CLIENT_ID environment variable or provide clientId in config.");
|
|
144
|
+
}
|
|
145
|
+
// Client secret is optional (for public clients using PKCE)
|
|
146
|
+
this.clientSecret =
|
|
147
|
+
config.clientSecret || process.env.ANTHROPIC_OAUTH_CLIENT_SECRET;
|
|
148
|
+
// Get redirect URI from config or environment or use official redirect URI
|
|
149
|
+
this.redirectUri =
|
|
150
|
+
config.redirectUri ||
|
|
151
|
+
process.env.ANTHROPIC_OAUTH_REDIRECT_URI ||
|
|
152
|
+
ANTHROPIC_REDIRECT_URI;
|
|
153
|
+
// Configure scopes
|
|
154
|
+
this.scopes = config.scopes || [...DEFAULT_SCOPES];
|
|
155
|
+
// Configure endpoints (using Claude Code's official endpoints)
|
|
156
|
+
this.authorizationUrl = config.authorizationUrl || ANTHROPIC_AUTH_URL;
|
|
157
|
+
this.tokenUrl = config.tokenUrl || ANTHROPIC_TOKEN_URL;
|
|
158
|
+
this.validationUrl =
|
|
159
|
+
config.validationUrl || "https://console.anthropic.com/v1/oauth/validate";
|
|
160
|
+
this.revocationUrl =
|
|
161
|
+
config.revocationUrl || "https://console.anthropic.com/v1/oauth/revoke";
|
|
162
|
+
logger.debug("AnthropicOAuth initialized", {
|
|
163
|
+
clientId: this.clientId.substring(0, 8) + "...",
|
|
164
|
+
redirectUri: this.redirectUri,
|
|
165
|
+
scopes: this.scopes,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// PKCE METHODS (STATIC)
|
|
170
|
+
// =============================================================================
|
|
171
|
+
/**
|
|
172
|
+
* Generates a cryptographically secure code verifier for PKCE
|
|
173
|
+
*
|
|
174
|
+
* The code verifier is a high-entropy random string between 43-128 characters
|
|
175
|
+
* using URL-safe characters (A-Z, a-z, 0-9, "-", ".", "_", "~").
|
|
176
|
+
*
|
|
177
|
+
* @returns A random code verifier string (64 characters)
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* const codeVerifier = AnthropicOAuth.generateCodeVerifier();
|
|
182
|
+
* // Returns something like "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
static generateCodeVerifier() {
|
|
186
|
+
// Generate 32 random bytes and convert to base64url (43-44 chars)
|
|
187
|
+
// Using 48 bytes gives us 64 characters which is well within spec
|
|
188
|
+
const buffer = randomBytes(48);
|
|
189
|
+
return buffer
|
|
190
|
+
.toString("base64")
|
|
191
|
+
.replace(/\+/g, "-")
|
|
192
|
+
.replace(/\//g, "_")
|
|
193
|
+
.replace(/=/g, "");
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Generates a PKCE code challenge from a code verifier
|
|
197
|
+
*
|
|
198
|
+
* Uses SHA-256 hashing as per RFC 7636. The challenge is the
|
|
199
|
+
* base64url-encoded SHA-256 hash of the code verifier.
|
|
200
|
+
*
|
|
201
|
+
* @param verifier - The code verifier to generate challenge from
|
|
202
|
+
* @returns Promise resolving to the code challenge string
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const verifier = AnthropicOAuth.generateCodeVerifier();
|
|
207
|
+
* const challenge = await AnthropicOAuth.generateCodeChallenge(verifier);
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
static async generateCodeChallenge(verifier) {
|
|
211
|
+
if (!verifier || verifier.length < 43 || verifier.length > 128) {
|
|
212
|
+
throw new OAuthError("Code verifier must be between 43-128 characters", "INVALID_CODE_VERIFIER");
|
|
213
|
+
}
|
|
214
|
+
// Create SHA-256 hash of the verifier
|
|
215
|
+
const hash = createHash("sha256").update(verifier).digest();
|
|
216
|
+
// Base64URL encode the hash
|
|
217
|
+
return hash
|
|
218
|
+
.toString("base64")
|
|
219
|
+
.replace(/\+/g, "-")
|
|
220
|
+
.replace(/\//g, "_")
|
|
221
|
+
.replace(/=/g, "");
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Generates both code verifier and challenge for PKCE
|
|
225
|
+
*
|
|
226
|
+
* Convenience method that generates both PKCE parameters at once.
|
|
227
|
+
*
|
|
228
|
+
* @returns Promise resolving to PKCE parameters object
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const pkce = await AnthropicOAuth.generatePKCE();
|
|
233
|
+
* console.log(pkce.codeVerifier);
|
|
234
|
+
* console.log(pkce.codeChallenge);
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
static async generatePKCE() {
|
|
238
|
+
const codeVerifier = AnthropicOAuth.generateCodeVerifier();
|
|
239
|
+
const codeChallenge = await AnthropicOAuth.generateCodeChallenge(codeVerifier);
|
|
240
|
+
return {
|
|
241
|
+
codeVerifier,
|
|
242
|
+
codeChallenge,
|
|
243
|
+
codeChallengeMethod: "S256",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// =============================================================================
|
|
247
|
+
// AUTHORIZATION URL GENERATION
|
|
248
|
+
// =============================================================================
|
|
249
|
+
/**
|
|
250
|
+
* Generates the OAuth authorization URL with PKCE support
|
|
251
|
+
*
|
|
252
|
+
* Builds the complete authorization URL including all required parameters
|
|
253
|
+
* for the OAuth 2.0 authorization code flow with PKCE.
|
|
254
|
+
*
|
|
255
|
+
* @param config - Authorization URL configuration
|
|
256
|
+
* @param state - Optional state parameter for CSRF protection
|
|
257
|
+
* @returns The complete authorization URL
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* const pkce = await AnthropicOAuth.generatePKCE();
|
|
262
|
+
* const authUrl = oauth.generateAuthUrl({
|
|
263
|
+
* codeChallenge: pkce.codeChallenge,
|
|
264
|
+
* state: crypto.randomUUID(),
|
|
265
|
+
* });
|
|
266
|
+
* // Redirect user to authUrl
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
generateAuthUrl(config = {}, state) {
|
|
270
|
+
// Generate state if not provided
|
|
271
|
+
const stateParam = state || this.generateState();
|
|
272
|
+
const params = new URLSearchParams({
|
|
273
|
+
response_type: "code",
|
|
274
|
+
client_id: this.clientId,
|
|
275
|
+
redirect_uri: this.redirectUri,
|
|
276
|
+
scope: this.scopes.join(" "),
|
|
277
|
+
state: stateParam,
|
|
278
|
+
});
|
|
279
|
+
// Add PKCE code challenge if provided
|
|
280
|
+
if (config.codeChallenge) {
|
|
281
|
+
params.append("code_challenge", config.codeChallenge);
|
|
282
|
+
params.append("code_challenge_method", "S256");
|
|
283
|
+
}
|
|
284
|
+
// Add any additional parameters
|
|
285
|
+
if (config.additionalParams) {
|
|
286
|
+
for (const [key, value] of Object.entries(config.additionalParams)) {
|
|
287
|
+
params.append(key, value);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const url = `${this.authorizationUrl}?${params.toString()}`;
|
|
291
|
+
logger.debug("Generated authorization URL", {
|
|
292
|
+
url: url.substring(0, 80) + "...",
|
|
293
|
+
hasPKCE: !!config.codeChallenge,
|
|
294
|
+
});
|
|
295
|
+
return url;
|
|
296
|
+
}
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// TOKEN EXCHANGE
|
|
299
|
+
// =============================================================================
|
|
300
|
+
/**
|
|
301
|
+
* Exchanges an authorization code for access and refresh tokens
|
|
302
|
+
*
|
|
303
|
+
* Performs the token exchange step of the OAuth flow. For public clients
|
|
304
|
+
* using PKCE, the code verifier must be provided.
|
|
305
|
+
*
|
|
306
|
+
* @param code - The authorization code from the OAuth callback
|
|
307
|
+
* @param codeVerifier - The PKCE code verifier used to generate the challenge
|
|
308
|
+
* @param config - Optional additional configuration
|
|
309
|
+
* @returns Promise resolving to the parsed OAuth tokens
|
|
310
|
+
* @throws OAuthTokenExchangeError if the exchange fails
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const tokens = await oauth.exchangeCodeForTokens(
|
|
315
|
+
* authorizationCode,
|
|
316
|
+
* pkce.codeVerifier
|
|
317
|
+
* );
|
|
318
|
+
* console.log("Access token:", tokens.accessToken);
|
|
319
|
+
* console.log("Expires at:", tokens.expiresAt);
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
async exchangeCodeForTokens(code, codeVerifier, config = {}) {
|
|
323
|
+
if (!code) {
|
|
324
|
+
throw new OAuthTokenExchangeError("Authorization code is required");
|
|
325
|
+
}
|
|
326
|
+
if (!codeVerifier) {
|
|
327
|
+
throw new OAuthTokenExchangeError("Code verifier is required for PKCE token exchange");
|
|
328
|
+
}
|
|
329
|
+
logger.debug("Exchanging authorization code for tokens");
|
|
330
|
+
const body = {
|
|
331
|
+
grant_type: "authorization_code",
|
|
332
|
+
code: code,
|
|
333
|
+
redirect_uri: config.redirectUri || this.redirectUri,
|
|
334
|
+
client_id: config.clientId || this.clientId,
|
|
335
|
+
code_verifier: codeVerifier,
|
|
336
|
+
};
|
|
337
|
+
// Add client secret if available (confidential clients)
|
|
338
|
+
const clientSecret = config.clientSecret || this.clientSecret;
|
|
339
|
+
if (clientSecret) {
|
|
340
|
+
body.client_secret = clientSecret;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const response = await fetch(config.tokenUrl || this.tokenUrl, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
347
|
+
Accept: "application/json",
|
|
348
|
+
},
|
|
349
|
+
body: new URLSearchParams(body).toString(),
|
|
350
|
+
});
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
const errorBody = await response.text();
|
|
353
|
+
logger.error("Token exchange failed", {
|
|
354
|
+
status: response.status,
|
|
355
|
+
error: redactTokens(errorBody).slice(0, 500),
|
|
356
|
+
});
|
|
357
|
+
throw new OAuthTokenExchangeError(`Token exchange failed: ${response.status} - ${errorBody}`, response.status);
|
|
358
|
+
}
|
|
359
|
+
const tokenResponse = await response.json();
|
|
360
|
+
const tokens = this.parseTokenResponse(tokenResponse);
|
|
361
|
+
logger.info("Token exchange successful", {
|
|
362
|
+
expiresAt: tokens.expiresAt.toISOString(),
|
|
363
|
+
hasRefreshToken: !!tokens.refreshToken,
|
|
364
|
+
});
|
|
365
|
+
return tokens;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (error instanceof OAuthError) {
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
throw new OAuthTokenExchangeError(`Failed to exchange authorization code: ${error instanceof Error ? error.message : String(error)}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// TOKEN REFRESH
|
|
376
|
+
// =============================================================================
|
|
377
|
+
/**
|
|
378
|
+
* Refreshes an expired access token using a refresh token
|
|
379
|
+
*
|
|
380
|
+
* @param refreshToken - The refresh token from a previous authentication
|
|
381
|
+
* @param config - Optional configuration overrides
|
|
382
|
+
* @returns Promise resolving to new OAuth tokens
|
|
383
|
+
* @throws OAuthTokenRefreshError if the refresh fails
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```typescript
|
|
387
|
+
* if (AnthropicOAuth.isTokenExpired(tokens.expiresAt)) {
|
|
388
|
+
* const newTokens = await oauth.refreshAccessToken(tokens.refreshToken);
|
|
389
|
+
* console.log("New access token:", newTokens.accessToken);
|
|
390
|
+
* }
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
async refreshAccessToken(refreshToken, config = {}) {
|
|
394
|
+
if (!refreshToken) {
|
|
395
|
+
throw new OAuthTokenRefreshError("Refresh token is required");
|
|
396
|
+
}
|
|
397
|
+
logger.debug("Refreshing access token");
|
|
398
|
+
const body = {
|
|
399
|
+
grant_type: "refresh_token",
|
|
400
|
+
refresh_token: refreshToken,
|
|
401
|
+
client_id: config.clientId || this.clientId,
|
|
402
|
+
};
|
|
403
|
+
// Add client secret if available
|
|
404
|
+
const clientSecret = config.clientSecret || this.clientSecret;
|
|
405
|
+
if (clientSecret) {
|
|
406
|
+
body.client_secret = clientSecret;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const response = await fetch(config.tokenUrl || this.tokenUrl, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: {
|
|
412
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
413
|
+
Accept: "application/json",
|
|
414
|
+
},
|
|
415
|
+
body: new URLSearchParams(body).toString(),
|
|
416
|
+
});
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
const errorBody = await response.text();
|
|
419
|
+
logger.error("Token refresh failed", {
|
|
420
|
+
status: response.status,
|
|
421
|
+
error: redactTokens(errorBody).slice(0, 500),
|
|
422
|
+
});
|
|
423
|
+
throw new OAuthTokenRefreshError(`Token refresh failed: ${response.status} - ${errorBody}`, response.status);
|
|
424
|
+
}
|
|
425
|
+
const tokenResponse = await response.json();
|
|
426
|
+
const tokens = this.parseTokenResponse(tokenResponse);
|
|
427
|
+
logger.info("Access token refreshed successfully", {
|
|
428
|
+
expiresAt: tokens.expiresAt.toISOString(),
|
|
429
|
+
});
|
|
430
|
+
return tokens;
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
if (error instanceof OAuthError) {
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
throw new OAuthTokenRefreshError(`Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// =============================================================================
|
|
440
|
+
// TOKEN VALIDATION
|
|
441
|
+
// =============================================================================
|
|
442
|
+
/**
|
|
443
|
+
* Validates an access token and returns token information
|
|
444
|
+
*
|
|
445
|
+
* Checks if the token is still valid by calling the validation endpoint.
|
|
446
|
+
* Returns user information if available.
|
|
447
|
+
*
|
|
448
|
+
* @param accessToken - The access token to validate
|
|
449
|
+
* @returns Promise resolving to validation result
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* const result = await oauth.validateToken(accessToken);
|
|
454
|
+
* if (result.isValid) {
|
|
455
|
+
* console.log("Token is valid, expires in:", result.expiresIn, "seconds");
|
|
456
|
+
* console.log("User email:", result.user?.email);
|
|
457
|
+
* } else {
|
|
458
|
+
* console.log("Token is invalid:", result.error);
|
|
459
|
+
* }
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
async validateToken(accessToken) {
|
|
463
|
+
if (!accessToken) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
logger.debug("Validating access token");
|
|
467
|
+
try {
|
|
468
|
+
const response = await fetch(this.validationUrl, {
|
|
469
|
+
method: "POST",
|
|
470
|
+
headers: {
|
|
471
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
472
|
+
Accept: "application/json",
|
|
473
|
+
Authorization: `Bearer ${accessToken}`,
|
|
474
|
+
},
|
|
475
|
+
body: new URLSearchParams({
|
|
476
|
+
token: accessToken,
|
|
477
|
+
}).toString(),
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
logger.debug("Token validation failed", {
|
|
481
|
+
status: response.status,
|
|
482
|
+
});
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
logger.debug("Token is valid");
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
logger.warn("Token validation request failed", {
|
|
490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
491
|
+
});
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Validates token and returns detailed information
|
|
497
|
+
*
|
|
498
|
+
* @param accessToken - The access token to validate
|
|
499
|
+
* @returns Promise resolving to detailed validation result
|
|
500
|
+
*/
|
|
501
|
+
async validateTokenWithDetails(accessToken) {
|
|
502
|
+
if (!accessToken) {
|
|
503
|
+
return {
|
|
504
|
+
isValid: false,
|
|
505
|
+
error: "Access token is required",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
logger.debug("Validating access token with details");
|
|
509
|
+
try {
|
|
510
|
+
const response = await fetch(this.validationUrl, {
|
|
511
|
+
method: "POST",
|
|
512
|
+
headers: {
|
|
513
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
514
|
+
Accept: "application/json",
|
|
515
|
+
Authorization: `Bearer ${accessToken}`,
|
|
516
|
+
},
|
|
517
|
+
body: new URLSearchParams({
|
|
518
|
+
token: accessToken,
|
|
519
|
+
}).toString(),
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
logger.debug("Token validation failed", {
|
|
523
|
+
status: response.status,
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
isValid: false,
|
|
527
|
+
error: `Token validation failed: ${response.status}`,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const validationData = await response.json();
|
|
531
|
+
return {
|
|
532
|
+
isValid: true,
|
|
533
|
+
expiresIn: validationData.expires_in,
|
|
534
|
+
scopes: validationData.scope?.split(" ") || [],
|
|
535
|
+
user: validationData.user
|
|
536
|
+
? {
|
|
537
|
+
id: validationData.user.id,
|
|
538
|
+
email: validationData.user.email,
|
|
539
|
+
subscription: validationData.user.subscription,
|
|
540
|
+
}
|
|
541
|
+
: undefined,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
logger.warn("Token validation request failed", {
|
|
546
|
+
error: error instanceof Error ? error.message : String(error),
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
isValid: false,
|
|
550
|
+
error: `Validation request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// =============================================================================
|
|
555
|
+
// TOKEN REVOCATION
|
|
556
|
+
// =============================================================================
|
|
557
|
+
/**
|
|
558
|
+
* Revokes an access token or refresh token
|
|
559
|
+
*
|
|
560
|
+
* @param token - The token to revoke
|
|
561
|
+
* @param tokenType - Type of token ("access_token" or "refresh_token")
|
|
562
|
+
* @returns Promise that resolves when revocation is complete
|
|
563
|
+
* @throws OAuthTokenRevocationError if revocation fails
|
|
564
|
+
*/
|
|
565
|
+
async revokeToken(token, tokenType = "access_token") {
|
|
566
|
+
if (!token) {
|
|
567
|
+
throw new OAuthTokenRevocationError("Token is required for revocation");
|
|
568
|
+
}
|
|
569
|
+
logger.debug("Revoking token", { tokenType });
|
|
570
|
+
const body = {
|
|
571
|
+
token: token,
|
|
572
|
+
token_type_hint: tokenType,
|
|
573
|
+
client_id: this.clientId,
|
|
574
|
+
};
|
|
575
|
+
if (this.clientSecret) {
|
|
576
|
+
body.client_secret = this.clientSecret;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const response = await fetch(this.revocationUrl, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: {
|
|
582
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
583
|
+
Accept: "application/json",
|
|
584
|
+
},
|
|
585
|
+
body: new URLSearchParams(body).toString(),
|
|
586
|
+
});
|
|
587
|
+
// RFC 7009: Revocation endpoint should return 200 even if token was already revoked
|
|
588
|
+
if (!response.ok && response.status !== 200) {
|
|
589
|
+
const errorBody = await response.text();
|
|
590
|
+
logger.error("Token revocation failed", {
|
|
591
|
+
status: response.status,
|
|
592
|
+
error: redactTokens(errorBody).slice(0, 500),
|
|
593
|
+
});
|
|
594
|
+
throw new OAuthTokenRevocationError(`Token revocation failed: ${response.status} - ${errorBody}`, response.status);
|
|
595
|
+
}
|
|
596
|
+
logger.info("Token revoked successfully", { tokenType });
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (error instanceof OAuthError) {
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
throw new OAuthTokenRevocationError(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// =============================================================================
|
|
606
|
+
// HELPER METHODS
|
|
607
|
+
// =============================================================================
|
|
608
|
+
/**
|
|
609
|
+
* Parses a token response into structured OAuthFlowTokens
|
|
610
|
+
*/
|
|
611
|
+
parseTokenResponse(response) {
|
|
612
|
+
const expiresAt = new Date(Date.now() + response.expires_in * 1000);
|
|
613
|
+
return {
|
|
614
|
+
accessToken: response.access_token,
|
|
615
|
+
tokenType: response.token_type || "Bearer",
|
|
616
|
+
expiresAt: expiresAt,
|
|
617
|
+
refreshToken: response.refresh_token,
|
|
618
|
+
scopes: response.scope?.split(" ") || this.scopes,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Generates a random state parameter for CSRF protection
|
|
623
|
+
*/
|
|
624
|
+
generateState() {
|
|
625
|
+
return randomBytes(32).toString("base64url");
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Checks if a token is expired or about to expire
|
|
629
|
+
*
|
|
630
|
+
* @param expiresAt - Token expiration date
|
|
631
|
+
* @param bufferSeconds - Buffer time before actual expiration (default: 60 seconds)
|
|
632
|
+
* @returns True if token is expired or will expire within buffer time
|
|
633
|
+
*/
|
|
634
|
+
static isTokenExpired(expiresAt, bufferSeconds = 60) {
|
|
635
|
+
const bufferMs = bufferSeconds * 1000;
|
|
636
|
+
return Date.now() >= expiresAt.getTime() - bufferMs;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Gets the configured client ID
|
|
640
|
+
*/
|
|
641
|
+
getClientId() {
|
|
642
|
+
return this.clientId;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Gets the configured redirect URI
|
|
646
|
+
*/
|
|
647
|
+
getRedirectUri() {
|
|
648
|
+
return this.redirectUri;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Gets the configured scopes
|
|
652
|
+
*/
|
|
653
|
+
getScopes() {
|
|
654
|
+
return this.scopes;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// =============================================================================
|
|
658
|
+
// LOCAL CALLBACK SERVER HELPER
|
|
659
|
+
// =============================================================================
|
|
660
|
+
/**
|
|
661
|
+
* Creates and starts a local HTTP server to receive OAuth callbacks
|
|
662
|
+
*
|
|
663
|
+
* This helper function starts a temporary HTTP server that listens for
|
|
664
|
+
* the OAuth callback and extracts the authorization code.
|
|
665
|
+
*
|
|
666
|
+
* @param port - Port to listen on (default: 8787)
|
|
667
|
+
* @param path - Path to listen on (default: "/callback")
|
|
668
|
+
* @param timeout - Timeout in milliseconds (default: 5 minutes)
|
|
669
|
+
* @returns Promise resolving to the callback result with authorization code
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```typescript
|
|
673
|
+
* // Start callback server before redirecting user
|
|
674
|
+
* const callbackPromise = startCallbackServer();
|
|
675
|
+
*
|
|
676
|
+
* // Generate auth URL and redirect user
|
|
677
|
+
* const authUrl = oauth.generateAuthUrl({ codeChallenge });
|
|
678
|
+
* console.log("Please visit:", authUrl);
|
|
679
|
+
*
|
|
680
|
+
* // Wait for callback
|
|
681
|
+
* const result = await callbackPromise;
|
|
682
|
+
* console.log("Got authorization code:", result.code);
|
|
683
|
+
*
|
|
684
|
+
* // Exchange for tokens
|
|
685
|
+
* const tokens = await oauth.exchangeCodeForTokens(result.code, codeVerifier);
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
export function startCallbackServer(port = DEFAULT_CALLBACK_PORT, path = "/callback", timeout = 5 * 60 * 1000) {
|
|
689
|
+
return new Promise((resolve, reject) => {
|
|
690
|
+
let server = null;
|
|
691
|
+
let timeoutId = null;
|
|
692
|
+
const cleanup = () => {
|
|
693
|
+
if (timeoutId) {
|
|
694
|
+
clearTimeout(timeoutId);
|
|
695
|
+
timeoutId = null;
|
|
696
|
+
}
|
|
697
|
+
if (server) {
|
|
698
|
+
server.close();
|
|
699
|
+
server = null;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
// Set timeout
|
|
703
|
+
timeoutId = setTimeout(() => {
|
|
704
|
+
cleanup();
|
|
705
|
+
reject(new OAuthCallbackServerError(`Callback server timed out after ${timeout / 1000} seconds`));
|
|
706
|
+
}, timeout);
|
|
707
|
+
server = createServer((req, res) => {
|
|
708
|
+
// Only handle the callback path
|
|
709
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
710
|
+
if (url.pathname !== path) {
|
|
711
|
+
res.writeHead(404);
|
|
712
|
+
res.end("Not Found");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
// Extract authorization code and state
|
|
716
|
+
const code = url.searchParams.get("code");
|
|
717
|
+
const state = url.searchParams.get("state");
|
|
718
|
+
const error = url.searchParams.get("error");
|
|
719
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
720
|
+
if (error) {
|
|
721
|
+
// OAuth error response — HTML-escape user-provided values to prevent XSS
|
|
722
|
+
const safeError = escapeHtml(error);
|
|
723
|
+
const safeDescription = errorDescription
|
|
724
|
+
? escapeHtml(errorDescription)
|
|
725
|
+
: "Please try again.";
|
|
726
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
727
|
+
res.end(`
|
|
728
|
+
<!DOCTYPE html>
|
|
729
|
+
<html>
|
|
730
|
+
<head><title>Authentication Error</title></head>
|
|
731
|
+
<body>
|
|
732
|
+
<h1>Authentication Failed</h1>
|
|
733
|
+
<p>Error: ${safeError}</p>
|
|
734
|
+
<p>${safeDescription}</p>
|
|
735
|
+
<p>You can close this window.</p>
|
|
736
|
+
</body>
|
|
737
|
+
</html>
|
|
738
|
+
`);
|
|
739
|
+
cleanup();
|
|
740
|
+
reject(new OAuthCallbackServerError(`OAuth error: ${error} - ${errorDescription}`));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (!code) {
|
|
744
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
745
|
+
res.end(`
|
|
746
|
+
<!DOCTYPE html>
|
|
747
|
+
<html>
|
|
748
|
+
<head><title>Missing Authorization Code</title></head>
|
|
749
|
+
<body>
|
|
750
|
+
<h1>Authentication Failed</h1>
|
|
751
|
+
<p>No authorization code received.</p>
|
|
752
|
+
<p>You can close this window.</p>
|
|
753
|
+
</body>
|
|
754
|
+
</html>
|
|
755
|
+
`);
|
|
756
|
+
cleanup();
|
|
757
|
+
reject(new OAuthCallbackServerError("No authorization code in callback"));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// Success response
|
|
761
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
762
|
+
res.end(`
|
|
763
|
+
<!DOCTYPE html>
|
|
764
|
+
<html>
|
|
765
|
+
<head><title>Authentication Successful</title></head>
|
|
766
|
+
<body>
|
|
767
|
+
<h1>Authentication Successful!</h1>
|
|
768
|
+
<p>You have been authenticated successfully.</p>
|
|
769
|
+
<p>You can close this window and return to the CLI.</p>
|
|
770
|
+
<script>window.close();</script>
|
|
771
|
+
</body>
|
|
772
|
+
</html>
|
|
773
|
+
`);
|
|
774
|
+
cleanup();
|
|
775
|
+
resolve({
|
|
776
|
+
code,
|
|
777
|
+
state: state || undefined,
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
server.on("error", (error) => {
|
|
781
|
+
cleanup();
|
|
782
|
+
reject(new OAuthCallbackServerError(`Failed to start callback server: ${error.message}`));
|
|
783
|
+
});
|
|
784
|
+
server.listen(port, () => {
|
|
785
|
+
logger.info(`OAuth callback server listening on port ${port}`);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Stops the callback server if running
|
|
791
|
+
* Note: The server automatically stops after receiving a callback or timing out
|
|
792
|
+
*/
|
|
793
|
+
export async function stopCallbackServer(server) {
|
|
794
|
+
return new Promise((resolve, reject) => {
|
|
795
|
+
server.close((error) => {
|
|
796
|
+
if (error) {
|
|
797
|
+
reject(new OAuthCallbackServerError(`Failed to stop callback server: ${error.message}`));
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
logger.info("OAuth callback server stopped");
|
|
801
|
+
resolve();
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
// =============================================================================
|
|
807
|
+
// HELPER FUNCTIONS
|
|
808
|
+
// =============================================================================
|
|
809
|
+
/**
|
|
810
|
+
* Creates an AnthropicOAuth instance with default configuration from environment
|
|
811
|
+
*
|
|
812
|
+
* @param overrides - Optional configuration overrides
|
|
813
|
+
* @returns Configured AnthropicOAuth instance
|
|
814
|
+
*
|
|
815
|
+
* @example
|
|
816
|
+
* ```typescript
|
|
817
|
+
* const oauth = createAnthropicOAuth();
|
|
818
|
+
* const authUrl = oauth.generateAuthUrl({ codeChallenge });
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
export function createAnthropicOAuth(overrides = {}) {
|
|
822
|
+
return new AnthropicOAuth(overrides);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Anthropic OAuth configuration creator for providerConfig pattern
|
|
826
|
+
*
|
|
827
|
+
* @returns Provider configuration options for Anthropic OAuth
|
|
828
|
+
*/
|
|
829
|
+
export function createAnthropicOAuthConfig() {
|
|
830
|
+
return {
|
|
831
|
+
providerName: "Anthropic OAuth",
|
|
832
|
+
envVarName: "ANTHROPIC_OAUTH_CLIENT_ID",
|
|
833
|
+
setupUrl: ANTHROPIC_OAUTH_BASE_URL,
|
|
834
|
+
description: "Claude Pro/Max OAuth Client Credentials",
|
|
835
|
+
instructions: [
|
|
836
|
+
`1. Visit: ${ANTHROPIC_OAUTH_BASE_URL}`,
|
|
837
|
+
"2. Create an OAuth application",
|
|
838
|
+
"3. Copy the Client ID",
|
|
839
|
+
`4. Set redirect URI to: ${DEFAULT_REDIRECT_URI}`,
|
|
840
|
+
"5. Set ANTHROPIC_OAUTH_CLIENT_ID environment variable",
|
|
841
|
+
],
|
|
842
|
+
fallbackEnvVars: [],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Checks if Anthropic OAuth credentials are configured
|
|
847
|
+
*
|
|
848
|
+
* @returns True if OAuth client ID is available
|
|
849
|
+
*/
|
|
850
|
+
export function hasAnthropicOAuthCredentials() {
|
|
851
|
+
return !!process.env.ANTHROPIC_OAUTH_CLIENT_ID;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Performs a complete OAuth flow including callback server
|
|
855
|
+
*
|
|
856
|
+
* This is a convenience function that handles the entire OAuth flow:
|
|
857
|
+
* 1. Generates PKCE parameters
|
|
858
|
+
* 2. Starts the callback server
|
|
859
|
+
* 3. Opens the browser (if possible)
|
|
860
|
+
* 4. Waits for the callback
|
|
861
|
+
* 5. Exchanges the code for tokens
|
|
862
|
+
*
|
|
863
|
+
* @param oauth - AnthropicOAuth instance
|
|
864
|
+
* @param options - Flow options
|
|
865
|
+
* @returns Promise resolving to OAuth tokens
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* ```typescript
|
|
869
|
+
* const oauth = createAnthropicOAuth();
|
|
870
|
+
* const tokens = await performOAuthFlow(oauth);
|
|
871
|
+
* console.log("Authenticated! Token expires at:", tokens.expiresAt);
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
874
|
+
export async function performOAuthFlow(oauth, options = {}) {
|
|
875
|
+
const { port = DEFAULT_CALLBACK_PORT, timeout = 5 * 60 * 1000, openBrowser = true, } = options;
|
|
876
|
+
// Generate PKCE parameters
|
|
877
|
+
const pkce = await AnthropicOAuth.generatePKCE();
|
|
878
|
+
// Generate state for CSRF protection
|
|
879
|
+
const state = randomBytes(32).toString("base64url");
|
|
880
|
+
// Start callback server
|
|
881
|
+
const callbackPromise = startCallbackServer(port, "/callback", timeout);
|
|
882
|
+
// Generate auth URL
|
|
883
|
+
const authUrl = oauth.generateAuthUrl({
|
|
884
|
+
codeChallenge: pkce.codeChallenge,
|
|
885
|
+
}, state);
|
|
886
|
+
// Try to open browser
|
|
887
|
+
if (openBrowser) {
|
|
888
|
+
try {
|
|
889
|
+
const open = (await import("open")).default;
|
|
890
|
+
await open(authUrl);
|
|
891
|
+
logger.info("Browser opened for authentication");
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
logger.warn("Could not open browser automatically");
|
|
895
|
+
logger.always("\nPlease open this URL in your browser to authenticate:");
|
|
896
|
+
logger.always(authUrl);
|
|
897
|
+
logger.always();
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
logger.always("\nPlease open this URL in your browser to authenticate:");
|
|
902
|
+
logger.always(authUrl);
|
|
903
|
+
logger.always();
|
|
904
|
+
}
|
|
905
|
+
// Wait for callback
|
|
906
|
+
const callbackResult = await callbackPromise;
|
|
907
|
+
// Verify state
|
|
908
|
+
if (!callbackResult.state || callbackResult.state !== state) {
|
|
909
|
+
throw new OAuthError("State mismatch - possible CSRF attack", "STATE_MISMATCH");
|
|
910
|
+
}
|
|
911
|
+
// Exchange code for tokens
|
|
912
|
+
const tokens = await oauth.exchangeCodeForTokens(callbackResult.code, pkce.codeVerifier);
|
|
913
|
+
return tokens;
|
|
914
|
+
}
|