@juspay/neurolink 9.41.0 → 9.42.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/CHANGELOG.md +8 -0
- package/README.md +7 -1
- package/dist/auth/anthropicOAuth.d.ts +18 -3
- package/dist/auth/anthropicOAuth.js +149 -4
- package/dist/auth/providers/firebase.js +5 -1
- package/dist/auth/providers/jwt.js +5 -1
- package/dist/auth/providers/workos.js +5 -1
- package/dist/auth/sessionManager.d.ts +1 -1
- package/dist/auth/sessionManager.js +58 -27
- package/dist/browser/neurolink.min.js +354 -334
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +188 -181
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +713 -431
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/factories/commandFactory.d.ts +2 -0
- package/dist/cli/factories/commandFactory.js +38 -0
- package/dist/cli/parser.js +4 -3
- package/dist/client/aiSdkAdapter.js +3 -0
- package/dist/client/streamingClient.js +30 -10
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +208 -230
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +138 -188
- package/dist/core/modules/GenerationHandler.js +3 -2
- package/dist/core/redisConversationMemoryManager.js +7 -3
- package/dist/evaluation/BatchEvaluator.js +4 -1
- package/dist/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/evaluation/pipeline/evaluationPipeline.js +24 -9
- package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
- package/dist/lib/auth/anthropicOAuth.js +149 -4
- package/dist/lib/auth/providers/firebase.js +5 -1
- package/dist/lib/auth/providers/jwt.js +5 -1
- package/dist/lib/auth/providers/workos.js +5 -1
- package/dist/lib/auth/sessionManager.d.ts +1 -1
- package/dist/lib/auth/sessionManager.js +58 -27
- package/dist/lib/client/aiSdkAdapter.js +3 -0
- package/dist/lib/client/streamingClient.js +30 -10
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +208 -230
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +138 -188
- package/dist/lib/core/modules/GenerationHandler.js +3 -2
- package/dist/lib/core/redisConversationMemoryManager.js +7 -3
- package/dist/lib/evaluation/BatchEvaluator.js +4 -1
- package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +24 -9
- package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +41 -2
- package/dist/lib/neurolink.js +1616 -1681
- package/dist/lib/observability/otelBridge.d.ts +2 -2
- package/dist/lib/observability/otelBridge.js +12 -3
- package/dist/lib/providers/amazonBedrock.js +2 -4
- package/dist/lib/providers/anthropic.d.ts +9 -5
- package/dist/lib/providers/anthropic.js +19 -14
- package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/lib/providers/anthropicBaseProvider.js +5 -4
- package/dist/lib/providers/azureOpenai.d.ts +1 -1
- package/dist/lib/providers/azureOpenai.js +5 -4
- package/dist/lib/providers/googleAiStudio.js +30 -6
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +437 -423
- package/dist/lib/providers/huggingFace.d.ts +3 -3
- package/dist/lib/providers/huggingFace.js +6 -8
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +76 -55
- package/dist/lib/providers/mistral.js +2 -1
- package/dist/lib/providers/ollama.js +93 -23
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +141 -141
- package/dist/lib/providers/openRouter.js +2 -1
- package/dist/lib/providers/openaiCompatible.d.ts +4 -4
- package/dist/lib/providers/openaiCompatible.js +4 -4
- package/dist/lib/proxy/claudeFormat.d.ts +3 -2
- package/dist/lib/proxy/claudeFormat.js +27 -14
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/lib/proxy/modelRouter.js +3 -0
- package/dist/lib/proxy/oauthFetch.d.ts +1 -1
- package/dist/lib/proxy/oauthFetch.js +289 -316
- package/dist/lib/proxy/proxyConfig.js +46 -24
- package/dist/lib/proxy/proxyEnv.d.ts +19 -0
- package/dist/lib/proxy/proxyEnv.js +73 -0
- package/dist/lib/proxy/proxyFetch.js +291 -217
- package/dist/lib/proxy/proxyTracer.d.ts +133 -0
- package/dist/lib/proxy/proxyTracer.js +645 -0
- package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/lib/proxy/rawStreamCapture.js +83 -0
- package/dist/lib/proxy/requestLogger.d.ts +32 -5
- package/dist/lib/proxy/requestLogger.js +503 -47
- package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
- package/dist/lib/proxy/sseInterceptor.js +427 -0
- package/dist/lib/proxy/usageStats.d.ts +4 -3
- package/dist/lib/proxy/usageStats.js +25 -12
- package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/lib/rag/chunking/markdownChunker.js +15 -6
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +17 -3
- package/dist/lib/server/routes/claudeProxyRoutes.js +3032 -1349
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/lib/services/server/ai/observability/instrumentation.js +337 -161
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +35 -22
- package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
- package/dist/lib/tasks/store/redisTaskStore.js +54 -39
- package/dist/lib/tasks/taskManager.d.ts +5 -0
- package/dist/lib/tasks/taskManager.js +158 -30
- package/dist/lib/telemetry/index.d.ts +2 -1
- package/dist/lib/telemetry/index.js +2 -1
- package/dist/lib/telemetry/telemetryService.d.ts +3 -0
- package/dist/lib/telemetry/telemetryService.js +69 -5
- package/dist/lib/types/cli.d.ts +10 -0
- package/dist/lib/types/proxyTypes.d.ts +160 -5
- package/dist/lib/types/streamTypes.d.ts +25 -3
- package/dist/lib/utils/messageBuilder.js +3 -2
- package/dist/lib/utils/providerHealth.d.ts +19 -0
- package/dist/lib/utils/providerHealth.js +279 -33
- package/dist/lib/utils/providerUtils.js +17 -22
- package/dist/lib/utils/toolChoice.d.ts +4 -0
- package/dist/lib/utils/toolChoice.js +7 -0
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +41 -2
- package/dist/neurolink.js +1616 -1681
- package/dist/observability/otelBridge.d.ts +2 -2
- package/dist/observability/otelBridge.js +12 -3
- package/dist/providers/amazonBedrock.js +2 -4
- package/dist/providers/anthropic.d.ts +9 -5
- package/dist/providers/anthropic.js +19 -14
- package/dist/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/providers/anthropicBaseProvider.js +5 -4
- package/dist/providers/azureOpenai.d.ts +1 -1
- package/dist/providers/azureOpenai.js +5 -4
- package/dist/providers/googleAiStudio.js +30 -6
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +437 -423
- package/dist/providers/huggingFace.d.ts +3 -3
- package/dist/providers/huggingFace.js +6 -7
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +76 -55
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/ollama.js +93 -23
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +141 -141
- package/dist/providers/openRouter.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +4 -4
- package/dist/providers/openaiCompatible.js +4 -3
- package/dist/proxy/claudeFormat.d.ts +3 -2
- package/dist/proxy/claudeFormat.js +27 -14
- package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/proxy/modelRouter.js +3 -0
- package/dist/proxy/oauthFetch.d.ts +1 -1
- package/dist/proxy/oauthFetch.js +289 -316
- package/dist/proxy/proxyConfig.js +46 -24
- package/dist/proxy/proxyEnv.d.ts +19 -0
- package/dist/proxy/proxyEnv.js +72 -0
- package/dist/proxy/proxyFetch.js +291 -217
- package/dist/proxy/proxyTracer.d.ts +133 -0
- package/dist/proxy/proxyTracer.js +644 -0
- package/dist/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/proxy/rawStreamCapture.js +82 -0
- package/dist/proxy/requestLogger.d.ts +32 -5
- package/dist/proxy/requestLogger.js +503 -47
- package/dist/proxy/sseInterceptor.d.ts +97 -0
- package/dist/proxy/sseInterceptor.js +426 -0
- package/dist/proxy/usageStats.d.ts +4 -3
- package/dist/proxy/usageStats.js +25 -12
- package/dist/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/rag/chunking/markdownChunker.js +15 -6
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -3
- package/dist/server/routes/claudeProxyRoutes.js +3032 -1349
- package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/services/server/ai/observability/instrumentation.js +337 -161
- package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
- package/dist/tasks/backends/bullmqBackend.js +35 -22
- package/dist/tasks/store/redisTaskStore.d.ts +1 -0
- package/dist/tasks/store/redisTaskStore.js +54 -39
- package/dist/tasks/taskManager.d.ts +5 -0
- package/dist/tasks/taskManager.js +158 -30
- package/dist/telemetry/index.d.ts +2 -1
- package/dist/telemetry/index.js +2 -1
- package/dist/telemetry/telemetryService.d.ts +3 -0
- package/dist/telemetry/telemetryService.js +69 -5
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/proxyTypes.d.ts +160 -5
- package/dist/types/streamTypes.d.ts +25 -3
- package/dist/utils/messageBuilder.js +3 -2
- package/dist/utils/providerHealth.d.ts +19 -0
- package/dist/utils/providerHealth.js +279 -33
- package/dist/utils/providerUtils.js +18 -22
- package/dist/utils/toolChoice.d.ts +4 -0
- package/dist/utils/toolChoice.js +6 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
- package/docs/changelog.md +252 -0
- package/package.json +19 -2
- package/scripts/observability/check-proxy-telemetry.mjs +235 -0
- package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
- package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
- package/scripts/observability/manage-local-openobserve.sh +215 -0
- package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
- package/scripts/observability/proxy-observability.env.example +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [9.42.1](https://github.com/juspay/neurolink/compare/v9.42.0...v9.42.1) (2026-04-02)
|
|
2
|
+
|
|
3
|
+
## [9.42.0](https://github.com/juspay/neurolink/compare/v9.41.0...v9.42.0) (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **(proxy):** add OTLP observability, passthrough mode, and env-file support ([59ae70b](https://github.com/juspay/neurolink/commit/59ae70b9a33089f04addce63eb9e6151dcc97a0f))
|
|
8
|
+
|
|
1
9
|
## [9.41.0](https://github.com/juspay/neurolink/compare/v9.40.0...v9.41.0) (2026-03-30)
|
|
2
10
|
|
|
3
11
|
### Features
|
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ Extracted from production systems at Juspay and battle-tested at enterprise scal
|
|
|
57
57
|
|
|
58
58
|
- **Memory** – Per-user condensed memory that persists across all conversations. Automatically retrieves and stores memory on each `generate()`/`stream()` call. Supports S3, Redis, and SQLite storage with LLM-powered condensation. → [Memory Guide](docs/features/memory.md)
|
|
59
59
|
- **External TracerProvider Support** – Integrate NeuroLink with applications that already have OpenTelemetry instrumentation. Supports auto-detection and manual configuration. → [Observability Guide](docs/features/observability.md)
|
|
60
|
+
- **Claude Proxy Telemetry** – Bootstrap a local OpenObserve + OTEL collector stack with `neurolink proxy telemetry setup`, import the maintained NeuroLink Proxy Observability dashboard, and inspect proxy logs, traces, metrics, cache reuse, and routing behavior. → [Claude Proxy Guide](docs/features/claude-proxy.md) | [Proxy Observability Guide](docs/features/claude-proxy-observability.md)
|
|
60
61
|
- **Server Adapters** – Deploy NeuroLink as an HTTP API server with your framework of choice (Hono, Express, Fastify, Koa). Full CLI support with `serve` and `server` commands for foreground/background modes, route management, and OpenAPI generation. → [Server Adapters Guide](docs/guides/server-adapters/index.md)
|
|
61
62
|
- **Title Generation Events** – Emit real-time events when conversation titles are auto-generated. Listen to `conversation:titleGenerated` for session tracking. → [Conversation Memory Guide](docs/conversation-memory.md#title-generation-events)
|
|
62
63
|
- **Custom Title Prompts** – Customize conversation title generation with `NEUROLINK_TITLE_PROMPT` environment variable. Use `${userMessage}` placeholder for dynamic prompts. → [Conversation Memory Guide](docs/conversation-memory.md#customizing-the-title-prompt)
|
|
@@ -599,6 +600,11 @@ npx @juspay/neurolink generate "Draft release notes" \
|
|
|
599
600
|
# RAG: Ask questions about your docs (auto-chunks, embeds, searches)
|
|
600
601
|
npx @juspay/neurolink generate "What are the key features?" \
|
|
601
602
|
--rag-files ./docs/guide.md ./docs/api.md --rag-strategy markdown
|
|
603
|
+
|
|
604
|
+
# Claude proxy + local OpenObserve dashboard
|
|
605
|
+
npx @juspay/neurolink proxy setup
|
|
606
|
+
npx @juspay/neurolink proxy telemetry setup
|
|
607
|
+
npx @juspay/neurolink proxy status --format json
|
|
602
608
|
```
|
|
603
609
|
|
|
604
610
|
```typescript
|
|
@@ -677,7 +683,7 @@ Full command and API breakdown lives in [`docs/cli/commands.md`](docs/cli/comman
|
|
|
677
683
|
| **Quality & governance** | Auto-evaluation engine (Q3), guardrails middleware (Q4), HITL workflows (Q4), audit logging. |
|
|
678
684
|
| **Memory & context** | Conversation memory, Redis history export (Q4), context summarization (Q4). |
|
|
679
685
|
| **CLI tooling** | Loop sessions (Q3), setup wizard, config validation, Redis auto-detect, JSON output. |
|
|
680
|
-
| **Enterprise ops** | Proxy support, regional routing (Q3), telemetry hooks, configuration management.
|
|
686
|
+
| **Enterprise ops** | Proxy support, regional routing (Q3), telemetry hooks, local OpenObserve dashboard setup, configuration management. |
|
|
681
687
|
| **Tool ecosystem** | MCP auto discovery, HTTP/stdio/SSE/WebSocket transports, LiteLLM hub access, SageMaker custom deployment, web search. |
|
|
682
688
|
|
|
683
689
|
## Documentation Map
|
|
@@ -43,13 +43,28 @@ export declare const DEFAULT_SCOPES: readonly string[];
|
|
|
43
43
|
/**
|
|
44
44
|
* User-Agent string to spoof Claude CLI
|
|
45
45
|
*/
|
|
46
|
-
export declare const
|
|
46
|
+
export declare const CLAUDE_CODE_VERSION = "2.1.87.6d6";
|
|
47
|
+
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
48
|
+
export declare const CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.87 (external, sdk-cli)";
|
|
49
|
+
export type ClaudeCodeIdentity = {
|
|
50
|
+
deviceId: string;
|
|
51
|
+
accountUuid: string;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
metadataUserId: string;
|
|
54
|
+
};
|
|
55
|
+
export declare function parseClaudeCodeUserId(userId: unknown): ClaudeCodeIdentity | null;
|
|
56
|
+
export declare function getOrCreateClaudeCodeIdentity(seed: string, options?: {
|
|
57
|
+
existingUserId?: unknown;
|
|
58
|
+
preferredSessionId?: string;
|
|
59
|
+
}): ClaudeCodeIdentity;
|
|
60
|
+
export declare function purgeExpiredClaudeCodeIdentities(now?: number): number;
|
|
61
|
+
export declare function buildStableClaudeCodeBillingHeader(originalText?: string): string;
|
|
47
62
|
/**
|
|
48
63
|
* Required beta headers for OAuth API requests.
|
|
49
64
|
* The "oauth-2025-04-20" header is CRITICAL for OAuth authentication.
|
|
50
|
-
* The "interleaved-thinking-2025-05-14" enables extended thinking.
|
|
51
65
|
*/
|
|
52
|
-
export declare const OAUTH_BETA_HEADERS = "oauth-2025-04-20
|
|
66
|
+
export declare const OAUTH_BETA_HEADERS = "oauth-2025-04-20";
|
|
67
|
+
export declare const CLAUDE_CODE_OAUTH_BETAS: readonly ["oauth-2025-04-20", "claude-code-20250219", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05", "advanced-tool-use-2025-11-20", "effort-2025-11-24"];
|
|
53
68
|
/**
|
|
54
69
|
* Tool name prefix required for OAuth API requests
|
|
55
70
|
*/
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module auth/anthropicOAuth
|
|
16
16
|
*/
|
|
17
|
-
import { createHash, randomBytes } from "crypto";
|
|
17
|
+
import { createHash, createHmac, randomBytes, randomUUID } from "crypto";
|
|
18
18
|
import { createServer } from "http";
|
|
19
19
|
import { OAuthError, OAuthConfigurationError, OAuthTokenExchangeError, OAuthTokenRefreshError, OAuthTokenRevocationError, OAuthCallbackServerError, } from "../types/errors.js";
|
|
20
20
|
import { logger } from "../utils/logger.js";
|
|
@@ -73,13 +73,158 @@ export const DEFAULT_SCOPES = [
|
|
|
73
73
|
/**
|
|
74
74
|
* User-Agent string to spoof Claude CLI
|
|
75
75
|
*/
|
|
76
|
-
export const
|
|
76
|
+
export const CLAUDE_CODE_VERSION = "2.1.87.6d6";
|
|
77
|
+
export const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
78
|
+
export const CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.87 (external, sdk-cli)";
|
|
79
|
+
const CLAUDE_CODE_IDENTITY_TTL_MS = 3_600_000;
|
|
80
|
+
const CLAUDE_CODE_IDENTITY_CACHE_MAX_ENTRIES = 1024;
|
|
81
|
+
const CLAUDE_CODE_IDENTITY_NAMESPACE = "neurolink-claude-code-identity-v1";
|
|
82
|
+
const claudeCodeIdentityCache = new Map();
|
|
83
|
+
function stableIdentityDigest(input) {
|
|
84
|
+
// These identifiers are deterministic pseudonyms for Claude Code metadata,
|
|
85
|
+
// not password hashes or authentication secrets.
|
|
86
|
+
return createHmac("sha256", CLAUDE_CODE_IDENTITY_NAMESPACE)
|
|
87
|
+
.update(input)
|
|
88
|
+
.digest("hex");
|
|
89
|
+
}
|
|
90
|
+
function hexToUuid(hex) {
|
|
91
|
+
const trimmed = hex.replace(/-/g, "").slice(0, 32).padEnd(32, "0");
|
|
92
|
+
return `${trimmed.slice(0, 8)}-${trimmed.slice(8, 12)}-${trimmed.slice(12, 16)}-${trimmed.slice(16, 20)}-${trimmed.slice(20, 32)}`;
|
|
93
|
+
}
|
|
94
|
+
function isUuid(value) {
|
|
95
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
96
|
+
}
|
|
97
|
+
function buildMetadataUserId(identity) {
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
device_id: identity.deviceId,
|
|
100
|
+
account_uuid: identity.accountUuid,
|
|
101
|
+
session_id: identity.sessionId,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function parseClaudeCodeUserId(userId) {
|
|
105
|
+
if (typeof userId !== "string") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(userId);
|
|
110
|
+
if (typeof parsed.device_id !== "string" ||
|
|
111
|
+
!/^[0-9a-f]{64}$/i.test(parsed.device_id) ||
|
|
112
|
+
typeof parsed.account_uuid !== "string" ||
|
|
113
|
+
!isUuid(parsed.account_uuid) ||
|
|
114
|
+
typeof parsed.session_id !== "string" ||
|
|
115
|
+
!isUuid(parsed.session_id)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
deviceId: parsed.device_id,
|
|
120
|
+
accountUuid: parsed.account_uuid,
|
|
121
|
+
sessionId: parsed.session_id,
|
|
122
|
+
metadataUserId: buildMetadataUserId({
|
|
123
|
+
deviceId: parsed.device_id,
|
|
124
|
+
accountUuid: parsed.account_uuid,
|
|
125
|
+
sessionId: parsed.session_id,
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function getOrCreateClaudeCodeIdentity(seed, options) {
|
|
134
|
+
const parsedExisting = parseClaudeCodeUserId(options?.existingUserId);
|
|
135
|
+
if (parsedExisting) {
|
|
136
|
+
if (options?.preferredSessionId && isUuid(options.preferredSessionId)) {
|
|
137
|
+
return {
|
|
138
|
+
deviceId: parsedExisting.deviceId,
|
|
139
|
+
accountUuid: parsedExisting.accountUuid,
|
|
140
|
+
sessionId: options.preferredSessionId,
|
|
141
|
+
metadataUserId: buildMetadataUserId({
|
|
142
|
+
deviceId: parsedExisting.deviceId,
|
|
143
|
+
accountUuid: parsedExisting.accountUuid,
|
|
144
|
+
sessionId: options.preferredSessionId,
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return parsedExisting;
|
|
149
|
+
}
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const cacheKey = seed || "default";
|
|
152
|
+
const cached = claudeCodeIdentityCache.get(cacheKey);
|
|
153
|
+
if (cached && cached.expiresAt > now) {
|
|
154
|
+
if (options?.preferredSessionId && isUuid(options.preferredSessionId)) {
|
|
155
|
+
return {
|
|
156
|
+
deviceId: cached.deviceId,
|
|
157
|
+
accountUuid: cached.accountUuid,
|
|
158
|
+
sessionId: options.preferredSessionId,
|
|
159
|
+
metadataUserId: buildMetadataUserId({
|
|
160
|
+
deviceId: cached.deviceId,
|
|
161
|
+
accountUuid: cached.accountUuid,
|
|
162
|
+
sessionId: options.preferredSessionId,
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return cached;
|
|
167
|
+
}
|
|
168
|
+
const deviceId = stableIdentityDigest(`${cacheKey}:device`);
|
|
169
|
+
const accountUuid = hexToUuid(stableIdentityDigest(`${cacheKey}:account`));
|
|
170
|
+
const sessionId = options?.preferredSessionId && isUuid(options.preferredSessionId)
|
|
171
|
+
? options.preferredSessionId
|
|
172
|
+
: randomUUID();
|
|
173
|
+
const identity = {
|
|
174
|
+
deviceId,
|
|
175
|
+
accountUuid,
|
|
176
|
+
sessionId,
|
|
177
|
+
metadataUserId: buildMetadataUserId({
|
|
178
|
+
deviceId,
|
|
179
|
+
accountUuid,
|
|
180
|
+
sessionId,
|
|
181
|
+
}),
|
|
182
|
+
expiresAt: now + CLAUDE_CODE_IDENTITY_TTL_MS,
|
|
183
|
+
};
|
|
184
|
+
enforceClaudeCodeIdentityCacheLimit(now);
|
|
185
|
+
claudeCodeIdentityCache.set(cacheKey, identity);
|
|
186
|
+
return identity;
|
|
187
|
+
}
|
|
188
|
+
export function purgeExpiredClaudeCodeIdentities(now = Date.now()) {
|
|
189
|
+
let removed = 0;
|
|
190
|
+
for (const [cacheKey, identity] of claudeCodeIdentityCache.entries()) {
|
|
191
|
+
if (identity.expiresAt <= now) {
|
|
192
|
+
claudeCodeIdentityCache.delete(cacheKey);
|
|
193
|
+
removed += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return removed;
|
|
197
|
+
}
|
|
198
|
+
function enforceClaudeCodeIdentityCacheLimit(now = Date.now()) {
|
|
199
|
+
purgeExpiredClaudeCodeIdentities(now);
|
|
200
|
+
while (claudeCodeIdentityCache.size >= CLAUDE_CODE_IDENTITY_CACHE_MAX_ENTRIES) {
|
|
201
|
+
const oldestKey = claudeCodeIdentityCache.keys().next().value;
|
|
202
|
+
if (!oldestKey) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
claudeCodeIdentityCache.delete(oldestKey);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function buildStableClaudeCodeBillingHeader(originalText) {
|
|
209
|
+
const version = originalText?.match(/cc_version=([^;]+)/)?.[1]?.trim() ||
|
|
210
|
+
CLAUDE_CODE_VERSION;
|
|
211
|
+
const entrypoint = originalText?.match(/cc_entrypoint=([^;]+)/)?.[1]?.trim() ||
|
|
212
|
+
CLAUDE_CODE_ENTRYPOINT;
|
|
213
|
+
return `x-anthropic-billing-header: cc_version=${version}; cc_entrypoint=${entrypoint}; cch=00000;`;
|
|
214
|
+
}
|
|
77
215
|
/**
|
|
78
216
|
* Required beta headers for OAuth API requests.
|
|
79
217
|
* The "oauth-2025-04-20" header is CRITICAL for OAuth authentication.
|
|
80
|
-
* The "interleaved-thinking-2025-05-14" enables extended thinking.
|
|
81
218
|
*/
|
|
82
|
-
export const OAUTH_BETA_HEADERS = "oauth-2025-04-20
|
|
219
|
+
export const OAUTH_BETA_HEADERS = "oauth-2025-04-20";
|
|
220
|
+
export const CLAUDE_CODE_OAUTH_BETAS = [
|
|
221
|
+
"oauth-2025-04-20",
|
|
222
|
+
"claude-code-20250219",
|
|
223
|
+
"context-management-2025-06-27",
|
|
224
|
+
"prompt-caching-scope-2026-01-05",
|
|
225
|
+
"advanced-tool-use-2025-11-20",
|
|
226
|
+
"effort-2025-11-24",
|
|
227
|
+
];
|
|
83
228
|
/**
|
|
84
229
|
* Tool name prefix required for OAuth API requests
|
|
85
230
|
*/
|
|
@@ -60,8 +60,12 @@ export class FirebaseAuthProvider extends BaseAuthProvider {
|
|
|
60
60
|
await this.initialize();
|
|
61
61
|
}
|
|
62
62
|
try {
|
|
63
|
+
const jwks = this.jwks;
|
|
64
|
+
if (!jwks) {
|
|
65
|
+
throw AuthError.create("PROVIDER_INIT_FAILED", "Firebase JWKS was not initialized", { details: { provider: "firebase" } });
|
|
66
|
+
}
|
|
63
67
|
// Verify the token using Google's public keys
|
|
64
|
-
const { payload } = await jose.jwtVerify(token,
|
|
68
|
+
const { payload } = await jose.jwtVerify(token, jwks, {
|
|
65
69
|
issuer: `https://securetoken.google.com/${this.projectId}`,
|
|
66
70
|
audience: this.projectId,
|
|
67
71
|
});
|
|
@@ -92,6 +92,10 @@ export class JWTProvider extends BaseAuthProvider {
|
|
|
92
92
|
await this.initialize();
|
|
93
93
|
}
|
|
94
94
|
try {
|
|
95
|
+
const keyObject = this.keyObject;
|
|
96
|
+
if (!keyObject) {
|
|
97
|
+
throw AuthError.create("PROVIDER_INIT_FAILED", "JWT verification key was not initialized", { details: { provider: "jwt" } });
|
|
98
|
+
}
|
|
95
99
|
const verifyOptions = {};
|
|
96
100
|
if (this.algorithms.length > 0) {
|
|
97
101
|
verifyOptions.algorithms = this
|
|
@@ -103,7 +107,7 @@ export class JWTProvider extends BaseAuthProvider {
|
|
|
103
107
|
if (this.audience) {
|
|
104
108
|
verifyOptions.audience = this.audience;
|
|
105
109
|
}
|
|
106
|
-
const { payload } = await jose.jwtVerify(token,
|
|
110
|
+
const { payload } = await jose.jwtVerify(token, keyObject, verifyOptions);
|
|
107
111
|
// Reject tokens without a non-empty sub claim
|
|
108
112
|
if (!payload.sub) {
|
|
109
113
|
return {
|
|
@@ -65,8 +65,12 @@ export class WorkOSProvider extends BaseAuthProvider {
|
|
|
65
65
|
await this.initialize();
|
|
66
66
|
}
|
|
67
67
|
try {
|
|
68
|
+
const jwks = this.jwks;
|
|
69
|
+
if (!jwks) {
|
|
70
|
+
throw AuthError.create("PROVIDER_INIT_FAILED", "WorkOS JWKS was not initialized", { details: { provider: "workos" } });
|
|
71
|
+
}
|
|
68
72
|
// Verify the JWT
|
|
69
|
-
const { payload } = await jose.jwtVerify(token,
|
|
73
|
+
const { payload } = await jose.jwtVerify(token, jwks, {
|
|
70
74
|
audience: this.clientId,
|
|
71
75
|
});
|
|
72
76
|
// Enforce organizationId if configured
|
|
@@ -46,7 +46,7 @@ export declare class MemorySessionStorage implements SessionManagerStorage {
|
|
|
46
46
|
* Redis session storage
|
|
47
47
|
*
|
|
48
48
|
* Distributed session storage using Redis. Suitable for multi-instance
|
|
49
|
-
* deployments. Requires
|
|
49
|
+
* deployments. Requires the "redis" (node-redis) package.
|
|
50
50
|
*
|
|
51
51
|
* Note: Redis client must be provided or configured via environment.
|
|
52
52
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/lib/auth/sessionManager.ts
|
|
2
|
+
import { withTimeout } from "../utils/async/withTimeout.js";
|
|
2
3
|
import { logger } from "../utils/logger.js";
|
|
3
4
|
/** Mask an identifier for safe logging: show first 4 chars + "***" */
|
|
4
5
|
function maskId(id) {
|
|
@@ -7,6 +8,7 @@ function maskId(id) {
|
|
|
7
8
|
}
|
|
8
9
|
return `${id.slice(0, 4)}***`;
|
|
9
10
|
}
|
|
11
|
+
const REDIS_CONNECT_TIMEOUT_MS = 5000;
|
|
10
12
|
/**
|
|
11
13
|
* In-memory session storage
|
|
12
14
|
*
|
|
@@ -31,10 +33,12 @@ export class MemorySessionStorage {
|
|
|
31
33
|
async set(session) {
|
|
32
34
|
this.sessions.set(session.id, session);
|
|
33
35
|
// Track user's sessions
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
let sessionIds = this.userSessions.get(session.user.id);
|
|
37
|
+
if (!sessionIds) {
|
|
38
|
+
sessionIds = new Set();
|
|
39
|
+
this.userSessions.set(session.user.id, sessionIds);
|
|
36
40
|
}
|
|
37
|
-
|
|
41
|
+
sessionIds.add(session.id);
|
|
38
42
|
}
|
|
39
43
|
async delete(sessionId) {
|
|
40
44
|
const session = this.sessions.get(sessionId);
|
|
@@ -84,7 +88,7 @@ export class MemorySessionStorage {
|
|
|
84
88
|
* Redis session storage
|
|
85
89
|
*
|
|
86
90
|
* Distributed session storage using Redis. Suitable for multi-instance
|
|
87
|
-
* deployments. Requires
|
|
91
|
+
* deployments. Requires the "redis" (node-redis) package.
|
|
88
92
|
*
|
|
89
93
|
* Note: Redis client must be provided or configured via environment.
|
|
90
94
|
*/
|
|
@@ -111,16 +115,25 @@ export class RedisSessionStorage {
|
|
|
111
115
|
async createClient() {
|
|
112
116
|
try {
|
|
113
117
|
// Use variable indirection to prevent TypeScript from resolving the module at compile time
|
|
114
|
-
const moduleName = "
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
const moduleName = "redis";
|
|
119
|
+
const redisModule = (await import(
|
|
120
|
+
/* @vite-ignore */ moduleName));
|
|
121
|
+
const client = redisModule.createClient({
|
|
122
|
+
url: this.redisUrl,
|
|
123
|
+
});
|
|
124
|
+
client.on("error", (err) => {
|
|
125
|
+
logger.error("Redis session client error:", err.message);
|
|
126
|
+
});
|
|
127
|
+
await withTimeout(client.connect(), REDIS_CONNECT_TIMEOUT_MS, `Redis session client connect timed out after ${REDIS_CONNECT_TIMEOUT_MS}ms`);
|
|
128
|
+
this.client = client;
|
|
129
|
+
return client;
|
|
119
130
|
}
|
|
120
|
-
catch {
|
|
131
|
+
catch (error) {
|
|
121
132
|
this.initPromise = null;
|
|
122
|
-
logger.error('Redis client
|
|
123
|
-
throw
|
|
133
|
+
logger.error('Redis client not available. Ensure the "redis" package is installed and Redis is reachable when using storage: "redis".');
|
|
134
|
+
throw error instanceof Error
|
|
135
|
+
? error
|
|
136
|
+
: new Error("Redis client not available");
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
139
|
sessionKey(sessionId) {
|
|
@@ -136,6 +149,13 @@ export class RedisSessionStorage {
|
|
|
136
149
|
if (!data) {
|
|
137
150
|
return null;
|
|
138
151
|
}
|
|
152
|
+
if (typeof data !== "string") {
|
|
153
|
+
logger.warn("Unexpected Redis session payload type", {
|
|
154
|
+
sessionId: maskId(sessionId),
|
|
155
|
+
type: typeof data,
|
|
156
|
+
});
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
139
159
|
const session = JSON.parse(data);
|
|
140
160
|
// Parse dates
|
|
141
161
|
session.createdAt = new Date(session.createdAt);
|
|
@@ -143,7 +163,7 @@ export class RedisSessionStorage {
|
|
|
143
163
|
session.expiresAt = new Date(session.expiresAt);
|
|
144
164
|
}
|
|
145
165
|
// Check expiration
|
|
146
|
-
if (new Date() > session.expiresAt) {
|
|
166
|
+
if (session.expiresAt && new Date() > session.expiresAt) {
|
|
147
167
|
await this.delete(sessionId);
|
|
148
168
|
return null;
|
|
149
169
|
}
|
|
@@ -162,9 +182,9 @@ export class RedisSessionStorage {
|
|
|
162
182
|
? Math.max(1, Math.floor((session.expiresAt.getTime() - Date.now()) / 1000))
|
|
163
183
|
: this.ttl;
|
|
164
184
|
// Store session
|
|
165
|
-
await client.
|
|
185
|
+
await client.setEx(this.sessionKey(session.id), ttlSeconds, JSON.stringify(session));
|
|
166
186
|
// Track user's sessions
|
|
167
|
-
await client.
|
|
187
|
+
await client.sAdd(this.userSessionsKey(session.user.id), session.id);
|
|
168
188
|
await client.expire(this.userSessionsKey(session.user.id), this.ttl);
|
|
169
189
|
}
|
|
170
190
|
catch (error) {
|
|
@@ -180,13 +200,21 @@ export class RedisSessionStorage {
|
|
|
180
200
|
// recursion for expired sessions.
|
|
181
201
|
const data = await client.get(this.sessionKey(sessionId));
|
|
182
202
|
if (data) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
if (typeof data !== "string") {
|
|
204
|
+
logger.warn("Unexpected Redis session payload type during delete", {
|
|
205
|
+
sessionId: maskId(sessionId),
|
|
206
|
+
type: typeof data,
|
|
207
|
+
});
|
|
186
208
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
else {
|
|
210
|
+
try {
|
|
211
|
+
const session = JSON.parse(data);
|
|
212
|
+
await client.sRem(this.userSessionsKey(session.user.id), sessionId);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// If parsing fails, we still delete the key below
|
|
216
|
+
logger.warn(`Failed to parse session data for cleanup: ${maskId(sessionId)}`);
|
|
217
|
+
}
|
|
190
218
|
}
|
|
191
219
|
}
|
|
192
220
|
await client.del(this.sessionKey(sessionId));
|
|
@@ -198,7 +226,7 @@ export class RedisSessionStorage {
|
|
|
198
226
|
async getUserSessions(userId) {
|
|
199
227
|
try {
|
|
200
228
|
const client = await this.getClient();
|
|
201
|
-
const sessionIds = await client.
|
|
229
|
+
const sessionIds = await client.sMembers(this.userSessionsKey(userId));
|
|
202
230
|
const sessions = [];
|
|
203
231
|
for (const sessionId of sessionIds) {
|
|
204
232
|
const session = await this.get(sessionId);
|
|
@@ -216,7 +244,7 @@ export class RedisSessionStorage {
|
|
|
216
244
|
async deleteUserSessions(userId) {
|
|
217
245
|
try {
|
|
218
246
|
const client = await this.getClient();
|
|
219
|
-
const sessionIds = await client.
|
|
247
|
+
const sessionIds = await client.sMembers(this.userSessionsKey(userId));
|
|
220
248
|
for (const sessionId of sessionIds) {
|
|
221
249
|
await client.del(this.sessionKey(sessionId));
|
|
222
250
|
}
|
|
@@ -232,10 +260,13 @@ export class RedisSessionStorage {
|
|
|
232
260
|
// Use SCAN instead of KEYS to avoid blocking Redis in production
|
|
233
261
|
let cursor = "0";
|
|
234
262
|
do {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
263
|
+
const result = await client.scan(cursor, {
|
|
264
|
+
MATCH: `${this.prefix}*`,
|
|
265
|
+
COUNT: 100,
|
|
266
|
+
});
|
|
267
|
+
cursor = result.cursor;
|
|
268
|
+
if (result.keys.length > 0) {
|
|
269
|
+
await client.del(result.keys);
|
|
239
270
|
}
|
|
240
271
|
} while (cursor !== "0");
|
|
241
272
|
}
|