Upload 15 files
Browse files
auth.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
| 1 |
-
import fs from 'fs';
|
| 2 |
-
import path from 'path';
|
| 3 |
-
import os from 'os';
|
| 4 |
-
import fetch from 'node-fetch';
|
| 5 |
-
import { logDebug, logError, logInfo } from './logger.js';
|
| 6 |
|
| 7 |
// State management for API key and refresh
|
| 8 |
-
let currentApiKey = null;
|
| 9 |
-
let currentRefreshToken = null;
|
| 10 |
-
let lastRefreshTime = null;
|
| 11 |
-
let clientId = null;
|
| 12 |
-
let authSource = null; // 'env' or 'file' or 'factory_key' or 'client'
|
| 13 |
-
let authFilePath = null;
|
| 14 |
let factoryApiKey = null; // 单密钥(兼容旧行为)
|
| 15 |
let factoryApiKeys = []; // 多密钥轮询列表
|
| 16 |
let factoryKeyIndex = 0; // 轮询指针
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
|
| 19 |
const REFRESH_INTERVAL_HOURS = 6; // Refresh every 6 hours
|
|
@@ -63,7 +66,7 @@ function generateClientId() {
|
|
| 63 |
* Load auth configuration with priority system
|
| 64 |
* Priority: FACTORY_API_KEY > refresh token mechanism > client authorization
|
| 65 |
*/
|
| 66 |
-
function loadAuthConfig() {
|
| 67 |
// 1. Check FACTORY_API_KEY environment variable (highest priority)
|
| 68 |
const factoryKey = process.env.FACTORY_API_KEY;
|
| 69 |
if (factoryKey && factoryKey.trim() !== '') {
|
|
@@ -124,7 +127,7 @@ function loadAuthConfig() {
|
|
| 124 |
logInfo('No auth configuration found, will use client authorization headers');
|
| 125 |
authSource = 'client';
|
| 126 |
return { type: 'client', value: null };
|
| 127 |
-
}
|
| 128 |
|
| 129 |
/**
|
| 130 |
* Refresh API key using refresh token
|
|
@@ -242,9 +245,9 @@ function shouldRefresh() {
|
|
| 242 |
/**
|
| 243 |
* Initialize auth system - load auth config and setup initial API key if needed
|
| 244 |
*/
|
| 245 |
-
export async function initializeAuth() {
|
| 246 |
-
try {
|
| 247 |
-
const authConfig = loadAuthConfig();
|
| 248 |
|
| 249 |
if (authConfig.type === 'factory_key') {
|
| 250 |
// FACTORY_API_KEY 模式:固定或轮询
|
|
@@ -265,18 +268,26 @@ export async function initializeAuth() {
|
|
| 265 |
logInfo('Auth system initialized for client authorization mode');
|
| 266 |
}
|
| 267 |
|
| 268 |
-
logInfo('Auth system initialized successfully');
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
/**
|
| 276 |
* Get API key based on configured authorization method
|
| 277 |
* @param {string} clientAuthorization - Authorization header from client request (optional)
|
| 278 |
*/
|
| 279 |
-
export async function getApiKey(clientAuthorization = null) {
|
| 280 |
// Priority 1: FACTORY_API_KEY environment variable
|
| 281 |
if (authSource === 'factory_key' && (factoryApiKey || factoryApiKeys.length > 0)) {
|
| 282 |
// 轮询选择密钥(若仅1个则等价于固定密钥)
|
|
@@ -310,5 +321,82 @@ export async function getApiKey(clientAuthorization = null) {
|
|
| 310 |
}
|
| 311 |
|
| 312 |
// No authorization available
|
| 313 |
-
throw new Error('No authorization available. Please configure FACTORY_API_KEY, refresh token, or provide client authorization.');
|
| 314 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import os from 'os';
|
| 4 |
+
import fetch from 'node-fetch';
|
| 5 |
+
import { logDebug, logError, logInfo } from './logger.js';
|
| 6 |
|
| 7 |
// State management for API key and refresh
|
| 8 |
+
let currentApiKey = null;
|
| 9 |
+
let currentRefreshToken = null;
|
| 10 |
+
let lastRefreshTime = null;
|
| 11 |
+
let clientId = null;
|
| 12 |
+
let authSource = null; // 'env' or 'file' or 'factory_key' or 'client'
|
| 13 |
+
let authFilePath = null;
|
| 14 |
let factoryApiKey = null; // 单密钥(兼容旧行为)
|
| 15 |
let factoryApiKeys = []; // 多密钥轮询列表
|
| 16 |
let factoryKeyIndex = 0; // 轮询指针
|
| 17 |
+
|
| 18 |
+
// 本服务对外提供的 API Key 访问控制(用于保护 /v1/* 入口)
|
| 19 |
+
let accessKeys = null; // Set<string> 或 null(未启用鉴权)
|
| 20 |
|
| 21 |
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
|
| 22 |
const REFRESH_INTERVAL_HOURS = 6; // Refresh every 6 hours
|
|
|
|
| 66 |
* Load auth configuration with priority system
|
| 67 |
* Priority: FACTORY_API_KEY > refresh token mechanism > client authorization
|
| 68 |
*/
|
| 69 |
+
function loadAuthConfig() {
|
| 70 |
// 1. Check FACTORY_API_KEY environment variable (highest priority)
|
| 71 |
const factoryKey = process.env.FACTORY_API_KEY;
|
| 72 |
if (factoryKey && factoryKey.trim() !== '') {
|
|
|
|
| 127 |
logInfo('No auth configuration found, will use client authorization headers');
|
| 128 |
authSource = 'client';
|
| 129 |
return { type: 'client', value: null };
|
| 130 |
+
}
|
| 131 |
|
| 132 |
/**
|
| 133 |
* Refresh API key using refresh token
|
|
|
|
| 245 |
/**
|
| 246 |
* Initialize auth system - load auth config and setup initial API key if needed
|
| 247 |
*/
|
| 248 |
+
export async function initializeAuth() {
|
| 249 |
+
try {
|
| 250 |
+
const authConfig = loadAuthConfig();
|
| 251 |
|
| 252 |
if (authConfig.type === 'factory_key') {
|
| 253 |
// FACTORY_API_KEY 模式:固定或轮询
|
|
|
|
| 268 |
logInfo('Auth system initialized for client authorization mode');
|
| 269 |
}
|
| 270 |
|
| 271 |
+
logInfo('Auth system initialized successfully');
|
| 272 |
+
|
| 273 |
+
// 载入对外访问的 API Key 列表(用于入站鉴权)
|
| 274 |
+
loadAccessKeysFromEnv();
|
| 275 |
+
if (accessKeys && accessKeys.size > 0) {
|
| 276 |
+
logInfo(`Inbound API Key enforcement enabled (${accessKeys.size} key(s))`);
|
| 277 |
+
} else {
|
| 278 |
+
logInfo('Inbound API Key not configured; API is publicly accessible');
|
| 279 |
+
}
|
| 280 |
+
} catch (error) {
|
| 281 |
+
logError('Failed to initialize auth system', error);
|
| 282 |
+
throw error;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
|
| 286 |
/**
|
| 287 |
* Get API key based on configured authorization method
|
| 288 |
* @param {string} clientAuthorization - Authorization header from client request (optional)
|
| 289 |
*/
|
| 290 |
+
export async function getApiKey(clientAuthorization = null) {
|
| 291 |
// Priority 1: FACTORY_API_KEY environment variable
|
| 292 |
if (authSource === 'factory_key' && (factoryApiKey || factoryApiKeys.length > 0)) {
|
| 293 |
// 轮询选择密钥(若仅1个则等价于固定密钥)
|
|
|
|
| 321 |
}
|
| 322 |
|
| 323 |
// No authorization available
|
| 324 |
+
throw new Error('No authorization available. Please configure FACTORY_API_KEY, refresh token, or provide client authorization.');
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/**
|
| 328 |
+
* 从环境变量加载对外访问的 API Key 列表
|
| 329 |
+
* 支持:
|
| 330 |
+
* - ACCESS_KEYS: 多个密钥,逗号/分号/空白分隔
|
| 331 |
+
* - ACCESS_KEY: 单个密钥
|
| 332 |
+
* 默认:未配置则不启用鉴权(保持向后兼容);在公开部署时建议务必配置。
|
| 333 |
+
*/
|
| 334 |
+
function loadAccessKeysFromEnv() {
|
| 335 |
+
const multi = process.env.ACCESS_KEYS;
|
| 336 |
+
const single = process.env.ACCESS_KEY;
|
| 337 |
+
|
| 338 |
+
let keys = [];
|
| 339 |
+
if (multi && multi.trim() !== '') {
|
| 340 |
+
keys = multi.split(/[\s,;]+/).map(k => k.trim()).filter(Boolean);
|
| 341 |
+
} else if (single && single.trim() !== '') {
|
| 342 |
+
keys = [single.trim()];
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
accessKeys = keys.length > 0 ? new Set(keys) : null;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
/**
|
| 349 |
+
* 提取客户端传入的访问密钥
|
| 350 |
+
* 支持:
|
| 351 |
+
* - 请求头 X-API-Key: <key>
|
| 352 |
+
* - Authorization: Bearer <key> | Api-Key <key> | Key <key>
|
| 353 |
+
* - 查询参数 api_key=<key>(可选,尽量用请求头)
|
| 354 |
+
*/
|
| 355 |
+
function extractClientAccessKey(req) {
|
| 356 |
+
const hdrKey = req.headers['x-api-key'] || req.headers['x-api_key'];
|
| 357 |
+
if (typeof hdrKey === 'string' && hdrKey.trim() !== '') {
|
| 358 |
+
return hdrKey.trim();
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
const auth = req.headers['authorization'];
|
| 362 |
+
if (typeof auth === 'string' && auth.trim() !== '') {
|
| 363 |
+
const m = auth.match(/^(Bearer|Api-Key|Key)\s+(.+)$/i);
|
| 364 |
+
if (m && m[2]) return m[2].trim();
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
if (req.query && typeof req.query.api_key === 'string' && req.query.api_key.trim() !== '') {
|
| 368 |
+
return req.query.api_key.trim();
|
| 369 |
+
}
|
| 370 |
+
return null;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/**
|
| 374 |
+
* 入站 API Key 鉴权中间件(保护 /v1/*)
|
| 375 |
+
* - 如未配置 ACCESS_KEYS/ACCESS_KEY,则放行且记录提醒日志
|
| 376 |
+
* - 如已配置,则要求客户端提供有效 Key,否则 401
|
| 377 |
+
*/
|
| 378 |
+
export function accessKeyMiddleware(req, res, next) {
|
| 379 |
+
// 仅在配置了密钥时启用
|
| 380 |
+
if (!accessKeys || accessKeys.size === 0) {
|
| 381 |
+
return next();
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
const key = extractClientAccessKey(req);
|
| 385 |
+
if (!key) {
|
| 386 |
+
res.setHeader('WWW-Authenticate', 'Bearer realm="droid2api"');
|
| 387 |
+
return res.status(401).json({
|
| 388 |
+
error: 'unauthorized',
|
| 389 |
+
message: 'Missing API key. Provide X-API-Key or Authorization: Bearer.'
|
| 390 |
+
});
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
if (!accessKeys.has(key)) {
|
| 394 |
+
return res.status(401).json({
|
| 395 |
+
error: 'unauthorized',
|
| 396 |
+
message: 'Invalid API key'
|
| 397 |
+
});
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// 通过校验
|
| 401 |
+
return next();
|
| 402 |
+
}
|
server.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
import { loadConfig, isDevMode, getPort } from './config.js';
|
| 3 |
import { logInfo, logError } from './logger.js';
|
| 4 |
-
import router from './routes.js';
|
| 5 |
-
import { initializeAuth } from './auth.js';
|
| 6 |
|
| 7 |
const app = express();
|
| 8 |
|
| 9 |
app.use(express.json({ limit: '50mb' }));
|
| 10 |
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
| 11 |
|
| 12 |
-
app.use((req, res, next) => {
|
| 13 |
-
res.header('Access-Control-Allow-Origin', '*');
|
| 14 |
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
| 15 |
-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, anthropic-version');
|
| 16 |
|
| 17 |
if (req.method === 'OPTIONS') {
|
| 18 |
return res.sendStatus(200);
|
| 19 |
}
|
| 20 |
-
next();
|
| 21 |
-
});
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
app.get('/', (req, res) => {
|
| 26 |
res.json({
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
import { loadConfig, isDevMode, getPort } from './config.js';
|
| 3 |
import { logInfo, logError } from './logger.js';
|
| 4 |
+
import router from './routes.js';
|
| 5 |
+
import { initializeAuth, accessKeyMiddleware } from './auth.js';
|
| 6 |
|
| 7 |
const app = express();
|
| 8 |
|
| 9 |
app.use(express.json({ limit: '50mb' }));
|
| 10 |
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
| 11 |
|
| 12 |
+
app.use((req, res, next) => {
|
| 13 |
+
res.header('Access-Control-Allow-Origin', '*');
|
| 14 |
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
| 15 |
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, anthropic-version');
|
| 16 |
|
| 17 |
if (req.method === 'OPTIONS') {
|
| 18 |
return res.sendStatus(200);
|
| 19 |
}
|
| 20 |
+
next();
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
// 入站 API Key 鉴权:仅保护 /v1/* 路由
|
| 24 |
+
app.use('/v1', accessKeyMiddleware);
|
| 25 |
+
|
| 26 |
+
app.use(router);
|
| 27 |
|
| 28 |
app.get('/', (req, res) => {
|
| 29 |
res.json({
|