|
|
const logger = require('../../utils/logger') |
|
|
const { CLIENT_DEFINITIONS } = require('../clientDefinitions') |
|
|
const { bestSimilarityByTemplates, SYSTEM_PROMPT_THRESHOLD } = require('../../utils/contents') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClaudeCodeValidator { |
|
|
|
|
|
|
|
|
|
|
|
static getId() { |
|
|
return CLIENT_DEFINITIONS.CLAUDE_CODE.id |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getName() { |
|
|
return CLIENT_DEFINITIONS.CLAUDE_CODE.name |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getDescription() { |
|
|
return CLIENT_DEFINITIONS.CLAUDE_CODE.description |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getIcon() { |
|
|
return CLIENT_DEFINITIONS.CLAUDE_CODE.icon || '🤖' |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static hasClaudeCodeSystemPrompt(body, customThreshold) { |
|
|
if (!body || typeof body !== 'object') { |
|
|
return false |
|
|
} |
|
|
|
|
|
const model = typeof body.model === 'string' ? body.model : null |
|
|
if (!model) { |
|
|
return false |
|
|
} |
|
|
|
|
|
const systemEntries = Array.isArray(body.system) ? body.system : null |
|
|
if (!systemEntries) { |
|
|
return false |
|
|
} |
|
|
|
|
|
const threshold = |
|
|
typeof customThreshold === 'number' && Number.isFinite(customThreshold) |
|
|
? customThreshold |
|
|
: SYSTEM_PROMPT_THRESHOLD |
|
|
|
|
|
for (const entry of systemEntries) { |
|
|
const rawText = typeof entry?.text === 'string' ? entry.text : '' |
|
|
const { bestScore } = bestSimilarityByTemplates(rawText) |
|
|
if (bestScore < threshold) { |
|
|
logger.error( |
|
|
`Claude system prompt similarity below threshold: score=${bestScore.toFixed(4)}, threshold=${threshold}, prompt=${rawText}` |
|
|
) |
|
|
return false |
|
|
} |
|
|
} |
|
|
return true |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static includesClaudeCodeSystemPrompt(body, customThreshold) { |
|
|
if (!body || typeof body !== 'object') { |
|
|
return false |
|
|
} |
|
|
|
|
|
const model = typeof body.model === 'string' ? body.model : null |
|
|
if (!model) { |
|
|
return false |
|
|
} |
|
|
|
|
|
const systemEntries = Array.isArray(body.system) ? body.system : null |
|
|
if (!systemEntries) { |
|
|
return false |
|
|
} |
|
|
|
|
|
const threshold = |
|
|
typeof customThreshold === 'number' && Number.isFinite(customThreshold) |
|
|
? customThreshold |
|
|
: SYSTEM_PROMPT_THRESHOLD |
|
|
|
|
|
let bestMatchScore = 0 |
|
|
|
|
|
for (const entry of systemEntries) { |
|
|
const rawText = typeof entry?.text === 'string' ? entry.text : '' |
|
|
const { bestScore } = bestSimilarityByTemplates(rawText) |
|
|
|
|
|
if (bestScore > bestMatchScore) { |
|
|
bestMatchScore = bestScore |
|
|
} |
|
|
|
|
|
if (bestScore >= threshold) { |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
logger.debug( |
|
|
`Claude system prompt not detected: bestScore=${bestMatchScore.toFixed(4)}, threshold=${threshold}` |
|
|
) |
|
|
|
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static validate(req) { |
|
|
try { |
|
|
const userAgent = req.headers['user-agent'] || '' |
|
|
const path = req.path || '' |
|
|
|
|
|
const claudeCodePattern = /^claude-cli\/\d+\.\d+\.\d+/i |
|
|
|
|
|
if (!claudeCodePattern.test(userAgent)) { |
|
|
|
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
if (!path.includes('messages')) { |
|
|
|
|
|
logger.debug(`Claude Code detected for path: ${path}, allowing access`) |
|
|
return true |
|
|
} |
|
|
|
|
|
|
|
|
if (!this.hasClaudeCodeSystemPrompt(req.body)) { |
|
|
logger.debug('Claude Code validation failed - missing or invalid Claude Code system prompt') |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
const xApp = req.headers['x-app'] |
|
|
const anthropicBeta = req.headers['anthropic-beta'] |
|
|
const anthropicVersion = req.headers['anthropic-version'] |
|
|
|
|
|
if (!xApp || xApp.trim() === '') { |
|
|
logger.debug('Claude Code validation failed - missing or empty x-app header') |
|
|
return false |
|
|
} |
|
|
|
|
|
if (!anthropicBeta || anthropicBeta.trim() === '') { |
|
|
logger.debug('Claude Code validation failed - missing or empty anthropic-beta header') |
|
|
return false |
|
|
} |
|
|
|
|
|
if (!anthropicVersion || anthropicVersion.trim() === '') { |
|
|
logger.debug('Claude Code validation failed - missing or empty anthropic-version header') |
|
|
return false |
|
|
} |
|
|
|
|
|
logger.debug( |
|
|
`Claude Code headers - x-app: ${xApp}, anthropic-beta: ${anthropicBeta}, anthropic-version: ${anthropicVersion}` |
|
|
) |
|
|
|
|
|
|
|
|
if (!req.body || !req.body.metadata || !req.body.metadata.user_id) { |
|
|
logger.debug('Claude Code validation failed - missing metadata.user_id in body') |
|
|
return false |
|
|
} |
|
|
|
|
|
const userId = req.body.metadata.user_id |
|
|
|
|
|
|
|
|
const userIdPattern = /^user_[a-fA-F0-9]{64}_account__session_[\w-]+$/ |
|
|
|
|
|
if (!userIdPattern.test(userId)) { |
|
|
logger.debug(`Claude Code validation failed - invalid user_id format: ${userId}`) |
|
|
|
|
|
|
|
|
if (!userId.startsWith('user_')) { |
|
|
logger.debug('user_id must start with "user_"') |
|
|
} else { |
|
|
const parts = userId.split('_') |
|
|
if (parts.length < 4) { |
|
|
logger.debug('user_id format is incomplete') |
|
|
} else if (parts[1].length !== 64) { |
|
|
logger.debug(`user hash must be 64 characters, got ${parts[1].length}`) |
|
|
} else if (parts[2] !== 'account' || parts[3] !== '' || parts[4] !== 'session') { |
|
|
logger.debug('user_id must contain "_account__session_"') |
|
|
} |
|
|
} |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
logger.debug(`Claude Code validation passed - UA: ${userAgent}, userId: ${userId}`) |
|
|
|
|
|
|
|
|
return true |
|
|
} catch (error) { |
|
|
logger.error('Error in ClaudeCodeValidator:', error) |
|
|
|
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getInfo() { |
|
|
return { |
|
|
id: this.getId(), |
|
|
name: this.getName(), |
|
|
description: this.getDescription(), |
|
|
icon: CLIENT_DEFINITIONS.CLAUDE_CODE.icon |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = ClaudeCodeValidator |
|
|
|