refactor(auth): 系统安全性增强
- 移除管理员凭证存储逻辑,改用 Cookie 管理会话 - 清理 admin-auth 中的存储函数,使用 Cookie 进行会话管理 - 更新 apiClient 请求逻辑,支持管理员 Cookie - 修改 auth store,调整登录和验证逻辑 - 添加 Cookie 处理函数,确保安全性和兼容性 - 更新测试脚本,验证 Cookie 功能
This commit is contained in:
1 parent
e46bcf0248
commit
8336649477
16 files changed
+412
-140
No files matched your search
@@ -14,7 +14,7 @@ import {
|
||||
UsageLogFilters,
|
||||
UsageLogSearchData,
|
||||
} from '@/types'
|
||||
import { clearAdminCredentials, getStoredAdminCredential } from '@/lib/admin-auth'
|
||||
import { clearAdminCredentials } from '@/lib/admin-auth'
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || ''
|
||||
|
||||
@@ -37,6 +37,8 @@ type RequestBehavior = {
|
||||
skipUnauthorizedHandler?: boolean
|
||||
}
|
||||
|
||||
const isAdminEndpoint = (endpoint: string): boolean => endpoint.startsWith('/api/admin/')
|
||||
|
||||
export function setErrorHandlers(handlers: {
|
||||
onUnauthorized?: () => void
|
||||
onError?: (error: ApiError) => void
|
||||
@@ -54,12 +56,6 @@ async function requestInterceptor(config: RequestInit): Promise<RequestInit> {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
// 添加 admin token
|
||||
const credential = getStoredAdminCredential()
|
||||
if (credential) {
|
||||
headers.set(credential.headerName, credential.token)
|
||||
}
|
||||
|
||||
return { ...config, headers }
|
||||
}
|
||||
|
||||
@@ -165,7 +161,10 @@ async function request<T>(
|
||||
|
||||
try {
|
||||
const config = await requestInterceptor(options)
|
||||
const response = await fetch(url, config)
|
||||
const requestConfig = isAdminEndpoint(endpoint)
|
||||
? { ...config, credentials: 'include' as const }
|
||||
: config
|
||||
const response = await fetch(url, requestConfig)
|
||||
return responseInterceptor(response, behavior)
|
||||
} catch (error) {
|
||||
// 处理网络错误
|
||||
@@ -291,6 +290,7 @@ export const apiClient = {
|
||||
const url = `${BASE_URL}${endpoint}`
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
||||
@@ -1,59 +1,10 @@
|
||||
export const ADMIN_SESSION_TOKEN_KEY = "adminSessionToken";
|
||||
export const LEGACY_ADMIN_TOKEN_KEY = "adminToken";
|
||||
export const ADMIN_SESSION_HEADER = "x-admin-session";
|
||||
export const LEGACY_ADMIN_TOKEN_HEADER = "x-admin-token";
|
||||
|
||||
const canUseLocalStorage = (): boolean => {
|
||||
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
||||
};
|
||||
|
||||
export const getStoredAdminSessionToken = (): string | null => {
|
||||
if (!canUseLocalStorage()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = window.localStorage.getItem(ADMIN_SESSION_TOKEN_KEY);
|
||||
return value && value.trim().length > 0 ? value.trim() : null;
|
||||
};
|
||||
|
||||
export const getStoredLegacyAdminToken = (): string | null => {
|
||||
if (!canUseLocalStorage()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = window.localStorage.getItem(LEGACY_ADMIN_TOKEN_KEY);
|
||||
return value && value.trim().length > 0 ? value.trim() : null;
|
||||
};
|
||||
|
||||
export const getStoredAdminCredential = (): { headerName: string; token: string } | null => {
|
||||
const sessionToken = getStoredAdminSessionToken();
|
||||
if (sessionToken) {
|
||||
return {
|
||||
headerName: ADMIN_SESSION_HEADER,
|
||||
token: sessionToken,
|
||||
};
|
||||
}
|
||||
|
||||
const legacyToken = getStoredLegacyAdminToken();
|
||||
if (legacyToken) {
|
||||
return {
|
||||
headerName: LEGACY_ADMIN_TOKEN_HEADER,
|
||||
token: legacyToken,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const storeAdminSessionToken = (token: string): void => {
|
||||
if (!canUseLocalStorage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.removeItem(LEGACY_ADMIN_TOKEN_KEY);
|
||||
window.localStorage.setItem(ADMIN_SESSION_TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
export const clearAdminCredentials = (): void => {
|
||||
if (!canUseLocalStorage()) {
|
||||
return;
|
||||
@@ -64,7 +15,9 @@ export const clearAdminCredentials = (): void => {
|
||||
};
|
||||
|
||||
export const getAdminCredentialScopeToken = (): string => {
|
||||
return getStoredAdminSessionToken()
|
||||
|| getStoredLegacyAdminToken()
|
||||
|| "anonymous";
|
||||
if (typeof window === "undefined") {
|
||||
return "server";
|
||||
}
|
||||
|
||||
return window.location.origin || "anonymous";
|
||||
};
|
||||
@@ -2,11 +2,7 @@ import { create } from 'zustand'
|
||||
import { apiClient } from '@/api/client'
|
||||
import { clearScopedCacheByPrefix } from '@/lib/local-cache'
|
||||
import { type AdminLoginResponse } from '@/types'
|
||||
import {
|
||||
clearAdminCredentials,
|
||||
getStoredAdminCredential,
|
||||
storeAdminSessionToken,
|
||||
} from '@/lib/admin-auth'
|
||||
import { clearAdminCredentials } from '@/lib/admin-auth'
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
@@ -38,13 +34,14 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
throw new Error('登录响应无效')
|
||||
}
|
||||
|
||||
if (loginResult.sessionToken) {
|
||||
storeAdminSessionToken(loginResult.sessionToken)
|
||||
clearAdminCredentials()
|
||||
|
||||
if (!loginResult.requiresVerification) {
|
||||
set({ isAuthenticated: true, isLoading: false, showAuthModal: false })
|
||||
return loginResult
|
||||
}
|
||||
|
||||
if (!loginResult.requiresVerification || !loginResult.challengeId) {
|
||||
if (!loginResult.challengeId) {
|
||||
throw new Error('登录响应缺少验证码挑战信息')
|
||||
}
|
||||
|
||||
@@ -68,11 +65,11 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
const response = await apiClient.verifyAdminLogin(challengeId, code)
|
||||
const loginResult = response.data as AdminLoginResponse
|
||||
|
||||
if (!loginResult.sessionToken) {
|
||||
throw new Error('登录响应缺少会话令牌')
|
||||
if (!loginResult || loginResult.requiresVerification) {
|
||||
throw new Error('验证码验证后未建立会话')
|
||||
}
|
||||
|
||||
storeAdminSessionToken(loginResult.sessionToken)
|
||||
clearAdminCredentials()
|
||||
set({ isAuthenticated: true, isLoading: false, showAuthModal: false })
|
||||
} catch (error) {
|
||||
clearAdminCredentials()
|
||||
@@ -99,13 +96,9 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
if (!getStoredAdminCredential()) {
|
||||
set({ isAuthenticated: false, isLoading: false })
|
||||
return
|
||||
}
|
||||
|
||||
set({ isLoading: true })
|
||||
try {
|
||||
clearAdminCredentials()
|
||||
await apiClient.checkAuth()
|
||||
set({ isAuthenticated: true, isLoading: false })
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,11 +5,13 @@ import { z } from "zod";
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
import { getSystemConfig, isTelegramSecurityEnabled } from "../system-config";
|
||||
import {
|
||||
clearAdminSessionCookie,
|
||||
createAdminLoginChallenge,
|
||||
createAdminSession,
|
||||
deleteAdminLoginChallenge,
|
||||
getAdminSessionHeaderName,
|
||||
getAdminSessionTokenFromRequest,
|
||||
invalidateAdminSession,
|
||||
setAdminSessionCookie,
|
||||
sendAdminLoginCodeNotification,
|
||||
sendAdminLoginResultNotification,
|
||||
verifyAdminLoginChallenge,
|
||||
@@ -36,6 +38,7 @@ const buildDirectLoginResponse = async (
|
||||
c: Context<HonoCustomType>
|
||||
) => {
|
||||
const session = await createAdminSession(c);
|
||||
setAdminSessionCookie(c, session.sessionToken, session.expiresAt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -43,7 +46,7 @@ const buildDirectLoginResponse = async (
|
||||
requiresVerification: false,
|
||||
challengeId: null,
|
||||
challengeExpiresAt: null,
|
||||
sessionToken: session.sessionToken,
|
||||
sessionToken: null,
|
||||
sessionExpiresAt: session.expiresAt,
|
||||
},
|
||||
message: "Login successful",
|
||||
@@ -188,6 +191,7 @@ export class AdminLoginVerifyEndpoint extends OpenAPIRoute {
|
||||
}
|
||||
|
||||
const session = await createAdminSession(c);
|
||||
setAdminSessionCookie(c, session.sessionToken, session.expiresAt);
|
||||
|
||||
await sendAdminLoginResultNotification(
|
||||
securityConfig,
|
||||
@@ -203,7 +207,7 @@ export class AdminLoginVerifyEndpoint extends OpenAPIRoute {
|
||||
requiresVerification: false,
|
||||
challengeId: null,
|
||||
challengeExpiresAt: null,
|
||||
sessionToken: session.sessionToken,
|
||||
sessionToken: null,
|
||||
sessionExpiresAt: session.expiresAt,
|
||||
},
|
||||
message: "Login verified successfully",
|
||||
@@ -222,9 +226,10 @@ export class AdminLogoutEndpoint extends OpenAPIRoute {
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const sessionToken = c.req.header(getAdminSessionHeaderName());
|
||||
const sessionToken = getAdminSessionTokenFromRequest(c);
|
||||
|
||||
await invalidateAdminSession(c, sessionToken);
|
||||
clearAdminSessionCookie(c);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Context } from "hono";
|
||||
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
||||
|
||||
const ADMIN_SESSION_HEADER = "x-admin-session";
|
||||
const ADMIN_SESSION_COOKIE_NAME = "oaw_admin_session";
|
||||
const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
||||
const ADMIN_LOGIN_CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
||||
const ADMIN_LOGIN_CHALLENGE_MAX_ATTEMPTS = 5;
|
||||
const DEFAULT_SYSTEM_TIMEZONE = "Asia/Shanghai";
|
||||
const LOCAL_DEV_HOSTNAMES = new Set(["0.0.0.0", "127.0.0.1", "::1", "localhost"]);
|
||||
|
||||
type LoginRequestMetadata = {
|
||||
clientIp: string;
|
||||
@@ -144,8 +146,52 @@ const buildBaseMessageLines = (
|
||||
];
|
||||
};
|
||||
|
||||
export const getAdminSessionHeaderName = (): string => {
|
||||
return ADMIN_SESSION_HEADER;
|
||||
const isSecureCookieRequest = (c: Context<HonoCustomType>): boolean => {
|
||||
const requestUrl = new URL(c.req.url);
|
||||
return requestUrl.protocol === "https:" && !LOCAL_DEV_HOSTNAMES.has(requestUrl.hostname);
|
||||
};
|
||||
|
||||
const buildAdminSessionCookieOptions = (
|
||||
c: Context<HonoCustomType>,
|
||||
expiresAt?: string
|
||||
) => {
|
||||
return {
|
||||
httpOnly: true,
|
||||
maxAge: Math.floor(ADMIN_SESSION_TTL_MS / 1000),
|
||||
path: "/api/admin",
|
||||
sameSite: "Lax" as const,
|
||||
secure: isSecureCookieRequest(c),
|
||||
...(expiresAt ? { expires: new Date(expiresAt) } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const getAdminSessionTokenFromRequest = (
|
||||
c: Context<HonoCustomType>
|
||||
): string | null => {
|
||||
return getCookie(c, ADMIN_SESSION_COOKIE_NAME) || null;
|
||||
};
|
||||
|
||||
export const setAdminSessionCookie = (
|
||||
c: Context<HonoCustomType>,
|
||||
sessionToken: string,
|
||||
expiresAt: string
|
||||
): void => {
|
||||
setCookie(
|
||||
c,
|
||||
ADMIN_SESSION_COOKIE_NAME,
|
||||
sessionToken,
|
||||
buildAdminSessionCookieOptions(c, expiresAt)
|
||||
);
|
||||
};
|
||||
|
||||
export const clearAdminSessionCookie = (
|
||||
c: Context<HonoCustomType>
|
||||
): void => {
|
||||
deleteCookie(
|
||||
c,
|
||||
ADMIN_SESSION_COOKIE_NAME,
|
||||
buildAdminSessionCookieOptions(c)
|
||||
);
|
||||
};
|
||||
|
||||
export const createAdminSession = async (
|
||||
|
||||
+10
-2
@@ -32,7 +32,11 @@ import {
|
||||
UsageLogSearchEndpoint,
|
||||
} from "./analytics_api"
|
||||
import { getSystemConfig, isTelegramSecurityEnabled } from "../system-config"
|
||||
import { getAdminSessionHeaderName, validateAdminSession } from "./auth_shared"
|
||||
import {
|
||||
clearAdminSessionCookie,
|
||||
getAdminSessionTokenFromRequest,
|
||||
validateAdminSession,
|
||||
} from "./auth_shared"
|
||||
|
||||
const app = new Hono<HonoCustomType>()
|
||||
export const api = fromHono(app)
|
||||
@@ -54,13 +58,17 @@ app.use('/api/admin/*', async (c, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionToken = c.req.header(getAdminSessionHeaderName());
|
||||
const sessionToken = getAdminSessionTokenFromRequest(c);
|
||||
|
||||
if (sessionToken) {
|
||||
if (await validateAdminSession(c, sessionToken)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
clearAdminSessionCookie(c);
|
||||
}
|
||||
|
||||
const systemConfig = await getSystemConfig(c);
|
||||
const securityEnabled = isTelegramSecurityEnabled(systemConfig.adminSecurity);
|
||||
const token = c.req.header('x-admin-token');
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Context } from "hono"
|
||||
import { checkoutUsageData, handleStreamResponse } from "./shared/openai-stream-utils"
|
||||
import { buildAzureTargetUrl } from "./shared/azure-target-url"
|
||||
import {
|
||||
buildUpstreamRequestHeaders,
|
||||
OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
} from "./shared/upstream-request-headers"
|
||||
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -10,11 +14,12 @@ const buildProxyRequest = (
|
||||
const targetUrl = buildAzureTargetUrl(request, config.endpoint)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Authorization")
|
||||
targetHeaders.delete("Host")
|
||||
targetHeaders.delete("Cookie")
|
||||
targetHeaders.set("api-key", apiKey)
|
||||
const targetHeaders = buildUpstreamRequestHeaders(request, {
|
||||
allowHeaders: OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
overrideHeaders: {
|
||||
"api-key": apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
checkoutResponsesUsageData,
|
||||
} from "./shared/responses-stream-utils"
|
||||
import { buildAzureTargetUrl } from "./shared/azure-target-url"
|
||||
import {
|
||||
buildUpstreamRequestHeaders,
|
||||
OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
} from "./shared/upstream-request-headers"
|
||||
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -13,11 +17,12 @@ const buildProxyRequest = (
|
||||
const targetUrl = buildAzureTargetUrl(request, config.endpoint)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Authorization")
|
||||
targetHeaders.delete("Host")
|
||||
targetHeaders.delete("Cookie")
|
||||
targetHeaders.set("api-key", apiKey)
|
||||
const targetHeaders = buildUpstreamRequestHeaders(request, {
|
||||
allowHeaders: OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
overrideHeaders: {
|
||||
"api-key": apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Context } from "hono"
|
||||
import { DEFAULT_CLAUDE_API_VERSION } from "../channel-config"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
import {
|
||||
buildUpstreamRequestHeaders,
|
||||
CLAUDE_UPSTREAM_HEADER_ALLOWLIST,
|
||||
} from "./shared/upstream-request-headers"
|
||||
|
||||
/**
|
||||
* Claude API Proxy Provider
|
||||
@@ -44,11 +48,7 @@ type ClaudeStreamEvent = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the proxy request for Claude API
|
||||
* - Removes Authorization header (client may send OpenAI format)
|
||||
* - Sets x-api-key header (Claude uses this instead of Authorization)
|
||||
* - Sets anthropic-version header (required by Claude API)
|
||||
* - Preserves all other headers from original request
|
||||
* Builds the proxy request for Claude API with an allowlisted header set.
|
||||
*/
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -59,16 +59,13 @@ const buildProxyRequest = (
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
|
||||
// Remove Authorization header (client may send OpenAI format: "Authorization: Bearer sk-xxx")
|
||||
targetHeaders.delete("Authorization")
|
||||
targetHeaders.delete("x-api-key")
|
||||
|
||||
// Claude uses x-api-key header instead of Authorization
|
||||
targetHeaders.set("x-api-key", apiKey)
|
||||
// Claude requires anthropic-version header
|
||||
targetHeaders.set("anthropic-version", DEFAULT_CLAUDE_API_VERSION)
|
||||
const targetHeaders = buildUpstreamRequestHeaders(request, {
|
||||
allowHeaders: CLAUDE_UPSTREAM_HEADER_ALLOWLIST,
|
||||
overrideHeaders: {
|
||||
"anthropic-version": DEFAULT_CLAUDE_API_VERSION,
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Context } from "hono"
|
||||
import { normalizeUsage as normalizeResponsesUsage } from "./shared/usage-utils"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
import {
|
||||
buildUpstreamRequestHeaders,
|
||||
OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
} from "./shared/upstream-request-headers"
|
||||
|
||||
type OpenAIStreamChoice = {
|
||||
index?: number;
|
||||
@@ -80,12 +84,13 @@ const buildProxyRequest = (
|
||||
config: ChannelConfig
|
||||
): Request => {
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, "/v1/chat/completions");
|
||||
const targetHeaders = new Headers(request.headers);
|
||||
const apiKey = config.api_key || "";
|
||||
|
||||
targetHeaders.delete("Authorization");
|
||||
targetHeaders.delete("x-api-key");
|
||||
targetHeaders.set("Authorization", `Bearer ${apiKey}`);
|
||||
const targetHeaders = buildUpstreamRequestHeaders(request, {
|
||||
allowHeaders: OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
overrideHeaders: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Context } from "hono"
|
||||
import { checkoutUsageData, handleStreamResponse } from "./shared/openai-stream-utils"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
import {
|
||||
buildUpstreamRequestHeaders,
|
||||
OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
} from "./shared/upstream-request-headers"
|
||||
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -11,10 +15,12 @@ const buildProxyRequest = (
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname, "/v1", config.type)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Host")
|
||||
targetHeaders.delete("Cookie")
|
||||
targetHeaders.set("Authorization", `Bearer ${apiKey}`)
|
||||
const targetHeaders = buildUpstreamRequestHeaders(request, {
|
||||
allowHeaders: OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
overrideHeaders: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
checkoutResponsesUsageData,
|
||||
} from "./shared/responses-stream-utils"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
import {
|
||||
buildUpstreamRequestHeaders,
|
||||
OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
} from "./shared/upstream-request-headers"
|
||||
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -14,10 +18,12 @@ const buildProxyRequest = (
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Host")
|
||||
targetHeaders.delete("Cookie")
|
||||
targetHeaders.set("Authorization", `Bearer ${apiKey}`)
|
||||
const targetHeaders = buildUpstreamRequestHeaders(request, {
|
||||
allowHeaders: OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST,
|
||||
overrideHeaders: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
const BLOCKED_HEADER_NAMES = new Set([
|
||||
"api-key",
|
||||
"authorization",
|
||||
"content-length",
|
||||
"cookie",
|
||||
"host",
|
||||
"true-client-ip",
|
||||
"x-admin-session",
|
||||
"x-admin-token",
|
||||
"x-api-key",
|
||||
"x-real-ip",
|
||||
]);
|
||||
|
||||
const BLOCKED_HEADER_PREFIXES = [
|
||||
"cf-",
|
||||
"x-forwarded-",
|
||||
];
|
||||
|
||||
const normalizeHeaderName = (value: string): string => {
|
||||
return value.trim().toLowerCase();
|
||||
};
|
||||
|
||||
const shouldForwardHeader = (
|
||||
name: string,
|
||||
allowlist: Set<string>
|
||||
): boolean => {
|
||||
if (!allowlist.has(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BLOCKED_HEADER_NAMES.has(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !BLOCKED_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix));
|
||||
};
|
||||
|
||||
type BuildUpstreamHeadersOptions = {
|
||||
allowHeaders: string[];
|
||||
overrideHeaders?: Record<string, string | null | undefined>;
|
||||
};
|
||||
|
||||
export const OPENAI_COMPAT_UPSTREAM_HEADER_ALLOWLIST = [
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"content-type",
|
||||
"idempotency-key",
|
||||
"openai-beta",
|
||||
];
|
||||
|
||||
export const CLAUDE_UPSTREAM_HEADER_ALLOWLIST = [
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"anthropic-beta",
|
||||
"content-type",
|
||||
"idempotency-key",
|
||||
];
|
||||
|
||||
export const buildUpstreamRequestHeaders = (
|
||||
request: Request,
|
||||
options: BuildUpstreamHeadersOptions
|
||||
): Headers => {
|
||||
const allowlist = new Set(options.allowHeaders.map(normalizeHeaderName));
|
||||
const headers = new Headers();
|
||||
|
||||
for (const [name, value] of request.headers.entries()) {
|
||||
const normalizedName = normalizeHeaderName(name);
|
||||
|
||||
if (!value || !shouldForwardHeader(normalizedName, allowlist)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.set(normalizedName, value);
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(options.overrideHeaders || {})) {
|
||||
const normalizedName = normalizeHeaderName(name);
|
||||
headers.delete(normalizedName);
|
||||
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
headers.set(normalizedName, value);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
@@ -5,6 +5,56 @@
|
||||
*/
|
||||
|
||||
const PORT = 9999
|
||||
const INSPECTABLE_REQUEST_KEYS = ["chat-completions", "messages", "models", "responses"] as const
|
||||
|
||||
type InspectableRequestKey = typeof INSPECTABLE_REQUEST_KEYS[number]
|
||||
|
||||
type CapturedRequest = {
|
||||
body: unknown
|
||||
headers: Record<string, string>
|
||||
method: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const capturedRequests = new Map<InspectableRequestKey, CapturedRequest>()
|
||||
|
||||
const toHeaderRecord = (headers: Headers): Record<string, string> => {
|
||||
return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value]))
|
||||
}
|
||||
|
||||
const getCaptureKey = (path: string): InspectableRequestKey | null => {
|
||||
if (path.endsWith("/chat/completions")) {
|
||||
return "chat-completions"
|
||||
}
|
||||
|
||||
if (path.endsWith("/messages")) {
|
||||
return "messages"
|
||||
}
|
||||
|
||||
if (path.endsWith("/responses")) {
|
||||
return "responses"
|
||||
}
|
||||
|
||||
if (path.endsWith("/models")) {
|
||||
return "models"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const rememberRequest = (req: Request, path: string, body: unknown) => {
|
||||
const captureKey = getCaptureKey(path)
|
||||
if (!captureKey) {
|
||||
return
|
||||
}
|
||||
|
||||
capturedRequests.set(captureKey, {
|
||||
body,
|
||||
headers: toHeaderRecord(req.headers),
|
||||
method: req.method,
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
// --- OpenAI Chat Completions (non-stream) ---
|
||||
const openaiChatResponse = (model: string) => ({
|
||||
@@ -87,11 +137,29 @@ const server = Bun.serve({
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
const path = url.pathname
|
||||
const inspectKey = url.searchParams.get("key")
|
||||
|
||||
if (path === "/__reset" && req.method === "POST") {
|
||||
capturedRequests.clear()
|
||||
return Response.json({ ok: true })
|
||||
}
|
||||
|
||||
if (path === "/__inspect" && req.method === "GET") {
|
||||
if (!inspectKey || !INSPECTABLE_REQUEST_KEYS.includes(inspectKey as InspectableRequestKey)) {
|
||||
return Response.json({
|
||||
keys: INSPECTABLE_REQUEST_KEYS,
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
return Response.json(capturedRequests.get(inspectKey as InspectableRequestKey) || null)
|
||||
}
|
||||
|
||||
const body = req.method === "POST" ? await req.json().catch(() => ({})) : {}
|
||||
const model = (body as any).model || "mock-model"
|
||||
const isStream = (body as any).stream === true
|
||||
|
||||
console.log(`[MOCK] ${req.method} ${path} model=${model} stream=${isStream}`)
|
||||
rememberRequest(req, path, body)
|
||||
|
||||
// OpenAI Chat Completions
|
||||
if (path.endsWith("/chat/completions")) {
|
||||
|
||||
+90
-8
@@ -2,7 +2,11 @@
|
||||
# E2E tests against mock upstream
|
||||
# Prerequisites: mock-upstream running on :9999, wrangler dev on :8788
|
||||
|
||||
API="http://localhost:8788"
|
||||
API="${API:-http://localhost:8787}"
|
||||
MOCK_API="${MOCK_API:-http://localhost:9999}"
|
||||
WRANGLER_CONFIG="${WRANGLER_CONFIG:-wrangler.local.jsonc}"
|
||||
DB_NAME="${DB_NAME:-one-api}"
|
||||
DB_EXEC=(npx wrangler d1 execute "$DB_NAME" --local --config "$WRANGLER_CONFIG" --command)
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TOTAL=0
|
||||
@@ -21,6 +25,20 @@ assert() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local name="$1" unexpected="$2" actual="$3"
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if echo "$actual" | grep -q "$unexpected"; then
|
||||
echo " ❌ #$TOTAL $name"
|
||||
echo " unexpected: $unexpected"
|
||||
echo " actual: $(echo "$actual" | head -1)"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
echo " ✅ #$TOTAL $name"
|
||||
PASS=$((PASS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_http() {
|
||||
local name="$1" expected_code="$2" expected_body="$3" actual="$4"
|
||||
local code=$(echo "$actual" | tail -1)
|
||||
@@ -39,6 +57,15 @@ assert_http() {
|
||||
|
||||
do_get() { curl -s -w "\n%{http_code}" "$@"; }
|
||||
do_post() { curl -s -w "\n%{http_code}" -X POST "$@"; }
|
||||
reset_mock_inspection() { curl -s -X POST "$MOCK_API/__reset" > /dev/null; }
|
||||
get_mock_inspection() { curl -sG "$MOCK_API/__inspect" --data-urlencode "key=$1"; }
|
||||
|
||||
if [[ -f ".dev.vars" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source ".dev.vars"
|
||||
set +a
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
@@ -78,10 +105,10 @@ R=$(do_post -H "Authorization: Bearer sk-mock-all" "$API/v1/chat/completions" -H
|
||||
assert_http "no model → 400" "400" "Model is required" "$R"
|
||||
|
||||
R=$(do_post -H "Authorization: Bearer sk-mock-all" "$API/v1/chat/completions" -H "Content-Type: application/json" -d '{"model":"nonexistent","messages":[]}')
|
||||
assert_http "unmapped model → 400" "400" "Model not mapped" "$R"
|
||||
assert_http "unmapped model → 400" "400" "Model not supported" "$R"
|
||||
|
||||
R=$(do_post -H "Authorization: Bearer sk-mock-openai" "$API/v1/chat/completions" -H "Content-Type: application/json" -d '{"model":"claude-3-opus","messages":[]}')
|
||||
assert_http "limited token, wrong model → 400" "400" "Model not mapped" "$R"
|
||||
assert_http "limited token, wrong model → 400" "400" "Model not supported" "$R"
|
||||
|
||||
# ---- /v1/chat/completions via openai provider (non-stream) ----
|
||||
echo ""
|
||||
@@ -97,6 +124,22 @@ echo "--- POST /v1/chat/completions (openai, stream) ---"
|
||||
R=$(do_post -H "Authorization: Bearer sk-mock-openai" "$API/v1/chat/completions" -H "Content-Type: application/json" -d '{"model":"gpt-4o","stream":true,"messages":[{"role":"user","content":"hi"}]}')
|
||||
assert_http "openai stream → 200 + SSE" "200" "data:" "$R"
|
||||
|
||||
reset_mock_inspection
|
||||
R=$(do_post \
|
||||
-H "Authorization: Bearer sk-mock-openai" \
|
||||
-H "x-api-key: leaked-client-key" \
|
||||
-H "Cookie: session=leak-me" \
|
||||
-H "X-Forwarded-For: 203.0.113.10" \
|
||||
"$API/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"header check"}]}')
|
||||
assert_http "openai header whitelist → 200" "200" "Mock response" "$R"
|
||||
INSPECT=$(get_mock_inspection "chat-completions")
|
||||
assert "openai upstream auth replaced with channel key" "\"authorization\":\"Bearer mock-key\"" "$INSPECT"
|
||||
assert_not_contains "openai upstream strips client x-api-key" "leaked-client-key" "$INSPECT"
|
||||
assert_not_contains "openai upstream strips cookie" "\"cookie\":" "$INSPECT"
|
||||
assert_not_contains "openai upstream strips x-forwarded-for" "\"x-forwarded-for\":" "$INSPECT"
|
||||
|
||||
# ---- /v1/chat/completions via azure-openai (non-stream) ----
|
||||
echo ""
|
||||
echo "--- POST /v1/chat/completions (azure-openai, non-stream) ---"
|
||||
@@ -125,6 +168,21 @@ echo "--- POST /v1/messages (claude, stream) ---"
|
||||
R=$(do_post -H "x-api-key: sk-mock-all" "$API/v1/messages" -H "Content-Type: application/json" -d '{"model":"claude-3-opus","stream":true,"max_tokens":100,"messages":[{"role":"user","content":"hi"}]}')
|
||||
assert_http "claude stream → 200 + SSE" "200" "event:" "$R"
|
||||
|
||||
reset_mock_inspection
|
||||
R=$(do_post \
|
||||
-H "x-api-key: sk-mock-all" \
|
||||
-H "Cookie: session=leak-me" \
|
||||
-H "X-Forwarded-For: 203.0.113.11" \
|
||||
"$API/v1/messages" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"claude-3-opus","max_tokens":100,"messages":[{"role":"user","content":"header check"}]}')
|
||||
assert_http "claude header whitelist → 200" "200" "Mock Claude response" "$R"
|
||||
INSPECT=$(get_mock_inspection "messages")
|
||||
assert "claude upstream x-api-key replaced with channel key" "\"x-api-key\":\"mock-key\"" "$INSPECT"
|
||||
assert_not_contains "claude upstream strips client token value" "sk-mock-all" "$INSPECT"
|
||||
assert_not_contains "claude upstream strips cookie" "\"cookie\":" "$INSPECT"
|
||||
assert_not_contains "claude upstream strips x-forwarded-for" "\"x-forwarded-for\":" "$INSPECT"
|
||||
|
||||
# ---- /v1/messages via claude-to-openai (non-stream) ----
|
||||
echo ""
|
||||
echo "--- POST /v1/messages (claude-to-openai, non-stream) ---"
|
||||
@@ -140,7 +198,7 @@ R=$(do_post "$API/v1/responses" -H "Content-Type: application/json" -d '{"model"
|
||||
assert_http "no auth → 401" "401" "Authorization header" "$R"
|
||||
|
||||
R=$(do_post -H "Authorization: Bearer sk-mock-all" "$API/v1/responses" -H "Content-Type: application/json" -d '{"model":"gpt-4o","input":"hi"}')
|
||||
assert_http "non-responses model → 400" "400" "Model not mapped" "$R"
|
||||
assert_http "non-responses model → 400" "400" "Model not supported" "$R"
|
||||
|
||||
# ---- /v1/responses via openai-responses (non-stream) ----
|
||||
echo ""
|
||||
@@ -174,10 +232,34 @@ assert_http "azure responses stream → 200 + SSE" "200" "event:" "$R"
|
||||
echo ""
|
||||
echo "--- Quota check ---"
|
||||
|
||||
npx wrangler d1 execute one-api-workers --local --command "UPDATE api_token SET usage = 99999999 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
|
||||
"${DB_EXEC[@]}" "UPDATE api_token SET usage = 99999999 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
|
||||
R=$(do_post -H "Authorization: Bearer sk-mock-all" "$API/v1/chat/completions" -H "Content-Type: application/json" -d '{"model":"gpt-4o","messages":[]}')
|
||||
assert_http "quota exceeded → 402" "402" "Quota exceeded" "$R"
|
||||
npx wrangler d1 execute one-api-workers --local --command "UPDATE api_token SET usage = 0 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
|
||||
"${DB_EXEC[@]}" "UPDATE api_token SET usage = 0 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
|
||||
|
||||
# ---- Admin cookie session ----
|
||||
echo ""
|
||||
echo "--- Admin session cookie ---"
|
||||
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO settings (key, value) VALUES ('SYSTEM_CONFIG', '{\"displayDecimals\":6,\"adminSecurity\":{\"enabled\":false,\"telegramBotToken\":\"\",\"telegramChatId\":\"\",\"verifiedFingerprint\":\"\",\"verifiedAt\":null},\"apiDocs\":{\"enabled\":true}}')" > /dev/null 2>&1
|
||||
COOKIE_JAR=$(mktemp)
|
||||
LOGIN_PAYLOAD=$(printf '{"token":"%s"}' "$ADMIN_TOKEN")
|
||||
|
||||
R=$(do_post "$API/api/admin/auth/login" -H "Content-Type: application/json" -c "$COOKIE_JAR" -d "$LOGIN_PAYLOAD")
|
||||
assert_http "admin login → 200" "200" "\"requiresVerification\":false" "$R"
|
||||
assert "admin login response keeps session token out of body" "\"sessionToken\":null" "$R"
|
||||
assert "admin login sets session cookie" "oaw_admin_session" "$(cat "$COOKIE_JAR")"
|
||||
|
||||
R=$(do_get -b "$COOKIE_JAR" "$API/api/admin/channel")
|
||||
assert_http "admin cookie grants access" "200" "\"success\":true" "$R"
|
||||
|
||||
R=$(do_post -b "$COOKIE_JAR" -c "$COOKIE_JAR" "$API/api/admin/auth/logout")
|
||||
assert_http "admin logout → 200" "200" "\"success\":true" "$R"
|
||||
|
||||
R=$(do_get -b "$COOKIE_JAR" "$API/api/admin/channel")
|
||||
assert_http "admin cookie invalid after logout" "401" "Unauthorized" "$R"
|
||||
|
||||
rm -f "$COOKIE_JAR"
|
||||
|
||||
# ---- Error recovery ----
|
||||
echo ""
|
||||
@@ -197,10 +279,10 @@ echo "========================================="
|
||||
echo ""
|
||||
echo "Cleaning up test data..."
|
||||
for key in test-openai test-azure test-claude test-c2o test-responses test-azure-resp; do
|
||||
npx wrangler d1 execute one-api-workers --local --command "DELETE FROM channel_config WHERE key = '$key'" > /dev/null 2>&1
|
||||
"${DB_EXEC[@]}" "DELETE FROM channel_config WHERE key = '$key'" > /dev/null 2>&1
|
||||
done
|
||||
for key in sk-mock-all sk-mock-openai sk-mock-quota; do
|
||||
npx wrangler d1 execute one-api-workers --local --command "DELETE FROM api_token WHERE key = '$key'" > /dev/null 2>&1
|
||||
"${DB_EXEC[@]}" "DELETE FROM api_token WHERE key = '$key'" > /dev/null 2>&1
|
||||
done
|
||||
echo "Cleanup done."
|
||||
|
||||
|
||||
@@ -3,34 +3,41 @@
|
||||
# All channels point to http://localhost:9999
|
||||
|
||||
MOCK="http://localhost:9999"
|
||||
API_URL="${API_URL:-http://localhost:8787}"
|
||||
WRANGLER_CONFIG="${WRANGLER_CONFIG:-wrangler.local.jsonc}"
|
||||
DB_NAME="${DB_NAME:-one-api}"
|
||||
DB_EXEC=(npx wrangler d1 execute "$DB_NAME" --local --config "$WRANGLER_CONFIG" --command)
|
||||
|
||||
echo "Setting up test channels and tokens..."
|
||||
|
||||
# Warm up the worker so schema migrations run against the same local D1 database.
|
||||
curl -s "$API_URL/api/admin/channel" > /dev/null 2>&1 || true
|
||||
|
||||
# Channel: openai type
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-openai', '{\"name\":\"test-openai\",\"type\":\"openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o\":\"gpt-4o\",\"gpt-4\":\"gpt-4\"}}')"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-openai', '{\"name\":\"test-openai\",\"type\":\"openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o\":\"gpt-4o\",\"gpt-4\":\"gpt-4\"}}')"
|
||||
|
||||
# Channel: azure-openai type
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure', '{\"name\":\"test-azure\",\"type\":\"azure-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2024-02-01\",\"deployment_mapper\":{\"gpt-4o-azure\":\"gpt-4o-deployment\"}}')"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure', '{\"name\":\"test-azure\",\"type\":\"azure-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2024-02-01\",\"deployment_mapper\":{\"gpt-4o-azure\":\"gpt-4o-deployment\"}}')"
|
||||
|
||||
# Channel: claude type
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-claude', '{\"name\":\"test-claude\",\"type\":\"claude\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-3-opus\":\"claude-3-opus-20240229\"}}')"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-claude', '{\"name\":\"test-claude\",\"type\":\"claude\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-3-opus\":\"claude-3-opus-20240229\"}}')"
|
||||
|
||||
# Channel: claude-to-openai type
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-c2o', '{\"name\":\"test-c2o\",\"type\":\"claude-to-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-via-openai\":\"claude-3-opus\"}}')"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-c2o', '{\"name\":\"test-c2o\",\"type\":\"claude-to-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-via-openai\":\"claude-3-opus\"}}')"
|
||||
|
||||
# Channel: openai-responses type
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-responses', '{\"name\":\"test-responses\",\"type\":\"openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o-resp\":\"gpt-4o\"}}')"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-responses', '{\"name\":\"test-responses\",\"type\":\"openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o-resp\":\"gpt-4o\"}}')"
|
||||
|
||||
# Channel: azure-openai-responses type
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure-resp', '{\"name\":\"test-azure-resp\",\"type\":\"azure-openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2025-01-01\",\"deployment_mapper\":{\"gpt-4o-azure-resp\":\"gpt-4o-deployment\"}}')"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure-resp', '{\"name\":\"test-azure-resp\",\"type\":\"azure-openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2025-01-01\",\"deployment_mapper\":{\"gpt-4o-azure-resp\":\"gpt-4o-deployment\"}}')"
|
||||
|
||||
# Token: access to all test channels only
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-all', '{\"name\":\"mock-all\",\"channel_keys\":[\"test-openai\",\"test-azure\",\"test-claude\",\"test-c2o\",\"test-responses\",\"test-azure-resp\"],\"total_quota\":99999999}', 0)"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-all', '{\"name\":\"mock-all\",\"channel_keys\":[\"test-openai\",\"test-azure\",\"test-claude\",\"test-c2o\",\"test-responses\",\"test-azure-resp\"],\"total_quota\":99999999}', 0)"
|
||||
|
||||
# Token: limited to openai only
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-openai', '{\"name\":\"mock-openai\",\"channel_keys\":[\"test-openai\"],\"total_quota\":99999999}', 0)"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-openai', '{\"name\":\"mock-openai\",\"channel_keys\":[\"test-openai\"],\"total_quota\":99999999}', 0)"
|
||||
|
||||
# Token: quota almost full
|
||||
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-quota', '{\"name\":\"mock-quota\",\"channel_keys\":[\"test-openai\"],\"total_quota\":100}', 99)"
|
||||
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-quota', '{\"name\":\"mock-quota\",\"channel_keys\":[\"test-openai\"],\"total_quota\":100}', 99)"
|
||||
|
||||
echo "Test data setup complete!"
|
||||
Reference in new issue
Block a user