Spaces:
Running
Running
File size: 4,372 Bytes
ceb3821 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | /**
* API 大锅饭 - 中间件模块
* 负责请求拦截和配额检查
*/
import { validateKey, incrementUsage, KEY_PREFIX } from './key-manager.js';
/**
* 从请求中提取 Potluck API Key
* 支持多种认证方式:
* 1. Authorization: Bearer maki_xxx
* 2. x-api-key: maki_xxx
* 3. x-goog-api-key: maki_xxx
* 4. URL query: ?key=maki_xxx
*
* @param {http.IncomingMessage} req - HTTP 请求对象
* @param {URL} requestUrl - 解析后的 URL 对象
* @returns {string|null} 提取到的 API Key,如果不是 potluck key 则返回 null
*/
export function extractPotluckKey(req, requestUrl) {
// 1. 检查 Authorization header
const authHeader = req.headers['authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
if (token.startsWith(KEY_PREFIX)) {
return token;
}
}
// 2. 检查 x-api-key header (Claude style)
const xApiKey = req.headers['x-api-key'];
if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) {
return xApiKey;
}
// 3. 检查 x-goog-api-key header (Gemini style)
const googApiKey = req.headers['x-goog-api-key'];
if (googApiKey && googApiKey.startsWith(KEY_PREFIX)) {
return googApiKey;
}
// 4. 检查 URL query parameter
const queryKey = requestUrl.searchParams.get('key');
if (queryKey && queryKey.startsWith(KEY_PREFIX)) {
return queryKey;
}
return null;
}
/**
* 检查请求是否使用 Potluck Key
* @param {http.IncomingMessage} req - HTTP 请求对象
* @param {URL} requestUrl - 解析后的 URL 对象
* @returns {boolean}
*/
export function isPotluckRequest(req, requestUrl) {
return extractPotluckKey(req, requestUrl) !== null;
}
/**
* Potluck 认证中间件
* 验证 Potluck API Key 并检查配额
*
* @param {http.IncomingMessage} req - HTTP 请求对象
* @param {URL} requestUrl - 解析后的 URL 对象
* @returns {Promise<{authorized: boolean, error?: Object, keyData?: Object, apiKey?: string}>}
*/
export async function potluckAuthMiddleware(req, requestUrl) {
const apiKey = extractPotluckKey(req, requestUrl);
if (!apiKey) {
// 不是 potluck 请求,返回 null 让原有逻辑处理
return { authorized: null };
}
// 验证 Key
const validation = await validateKey(apiKey);
if (!validation.valid) {
const errorMessages = {
'invalid_format': 'Invalid API key format',
'not_found': 'API key not found',
'disabled': 'API key has been disabled',
'quota_exceeded': 'Quota exceeded for this API key'
};
const statusCodes = {
'invalid_format': 401,
'not_found': 401,
'disabled': 403,
'quota_exceeded': 429
};
return {
authorized: false,
error: {
statusCode: statusCodes[validation.reason] || 401,
message: errorMessages[validation.reason] || 'Authentication failed',
code: validation.reason,
keyData: validation.keyData
}
};
}
return {
authorized: true,
keyData: validation.keyData,
apiKey: apiKey
};
}
/**
* 记录 Potluck 请求使用
* 在请求成功处理后调用
*
* @param {string} apiKey - API Key
* @returns {Promise<Object|null>}
*/
export async function recordPotluckUsage(apiKey) {
if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) {
return null;
}
return incrementUsage(apiKey);
}
/**
* 创建 Potluck 错误响应
* @param {http.ServerResponse} res - HTTP 响应对象
* @param {Object} error - 错误信息
*/
export function sendPotluckError(res, error) {
const response = {
error: {
message: error.message,
code: error.code,
type: 'potluck_error'
}
};
// 如果是配额超限,添加额外信息
if (error.code === 'quota_exceeded' && error.keyData) {
response.error.quota = {
used: error.keyData.todayUsage,
limit: error.keyData.dailyLimit,
resetDate: error.keyData.lastResetDate
};
}
res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
}
|