feat: add verified profile import and export
This commit is contained in:
@@ -4,7 +4,7 @@ import http from "node:http";
|
||||
import net from "node:net";
|
||||
import { once } from "node:events";
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -801,6 +801,134 @@ async function run() {
|
||||
"文本 profile 保存不应写入图片配置",
|
||||
);
|
||||
|
||||
const exportableTextProfileResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: "export-source",
|
||||
listen_host: "127.0.0.1",
|
||||
listen_port: gatewayPort,
|
||||
upstream_base_url: `http://127.0.0.1:${upstreamPort}`,
|
||||
auth_mode: "manual_bearer",
|
||||
manual_secret: "text-export-secret",
|
||||
management_access_key: "profile-admin-secret",
|
||||
request_history_limit: 25,
|
||||
model_remap: "gpt-test=gpt-upstream",
|
||||
reasoning_match_mode: "manual",
|
||||
reasoning_equals: [516],
|
||||
retryable_status_codes: [429, 503],
|
||||
retryable_error_messages: ["capacity test"],
|
||||
upstream_fetch_retry_attempts: 3,
|
||||
upstream_fetch_retry_backoff_ms: 50,
|
||||
endpoints: ["/responses", "/v1/responses"],
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert(exportableTextProfileResponse.status === 200, `可导出文本 profile 保存失败: ${exportableTextProfileResponse.status}`);
|
||||
const exportableTextProfilePayload = await exportableTextProfileResponse.json();
|
||||
assert(!JSON.stringify(exportableTextProfilePayload).includes("text-export-secret"), "普通 profile API 不应返回导出 key");
|
||||
|
||||
const passthroughExportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles/export`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ profile: "legacy-text", verification_key: "normal-upstream-key" }),
|
||||
},
|
||||
);
|
||||
assert(passthroughExportResponse.status === 400, "passthrough profile 不应允许导出不可验证的 key");
|
||||
const passthroughExportPayload = await passthroughExportResponse.json();
|
||||
assert(
|
||||
passthroughExportPayload?.error?.code === "profile_export_key_unavailable",
|
||||
"passthrough profile 未返回 key unavailable 标识",
|
||||
);
|
||||
|
||||
const wrongTextExportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles/export`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ profile: "export-source", verification_key: "wrong-export-secret" }),
|
||||
},
|
||||
);
|
||||
assert(wrongTextExportResponse.status === 403, `错误文本 key 应拒绝导出: ${wrongTextExportResponse.status}`);
|
||||
const wrongTextExportPayload = await wrongTextExportResponse.json();
|
||||
assert(wrongTextExportPayload?.error?.code === "profile_export_key_invalid", "错误文本 key 未返回验证失败标识");
|
||||
assert(!JSON.stringify(wrongTextExportPayload).includes("text-export-secret"), "错误 key 响应泄露了文本 profile key");
|
||||
|
||||
const textExportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles/export`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ profile: "export-source", verification_key: "text-export-secret" }),
|
||||
},
|
||||
);
|
||||
assert(textExportResponse.status === 200, `文本 profile 导出失败: ${textExportResponse.status}`);
|
||||
assert((textExportResponse.headers.get("cache-control") || "").includes("no-store"), "文本导出响应未禁用缓存");
|
||||
assert(
|
||||
(textExportResponse.headers.get("content-disposition") || "").includes("export-source.profile.json"),
|
||||
"文本导出响应缺少下载文件名",
|
||||
);
|
||||
const textExportBundle = await textExportResponse.json();
|
||||
assert(textExportBundle?.format === "codex-retry-gateway-profile", "文本导出格式标识错误");
|
||||
assert(textExportBundle?.version === 1 && textExportBundle?.kind === "text", "文本导出版本或 kind 错误");
|
||||
assert(textExportBundle?.profile?.key === "text-export-secret", "文本导出文件未包含 API key");
|
||||
assert(textExportBundle?.profile?.management_access_key === "profile-admin-secret", "文本导出文件未保留管理 access key");
|
||||
assert(textExportBundle?.profile?.auth_mode === "manual_bearer", "文本导出文件未转换为可移植认证模式");
|
||||
assert(textExportBundle?.profile?.auth_file === undefined, "文本导出文件不应包含源机器 secret 路径");
|
||||
|
||||
const textImportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles/import`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ bundle: textExportBundle, name: "imported-text" }),
|
||||
},
|
||||
);
|
||||
assert(textImportResponse.status === 200, `文本 profile 导入失败: ${textImportResponse.status}`);
|
||||
const textImportPayload = await textImportResponse.json();
|
||||
const importedTextProfile = (textImportPayload.profiles || []).find((profile) => profile?.name === "imported-text");
|
||||
assert(importedTextProfile?.form?.auth_mode === "manual_bearer", "导入文本 profile 未使用 manual_bearer");
|
||||
assert(importedTextProfile?.form?.manual_secret_configured === true, "导入文本 profile 未配置本机 secret");
|
||||
assert(!JSON.stringify(textImportPayload).includes("text-export-secret"), "文本导入响应泄露了 API key");
|
||||
const importedTextEnv = await readFile(path.join(profilesDir, "imported-text.env"), "utf8");
|
||||
assert(!importedTextEnv.includes("text-export-secret"), "导入文本 profile env 不应包含明文 API key");
|
||||
assert(importedTextEnv.includes("CODEX_RETRY_GATEWAY_MANAGEMENT_ACCESS_KEY=profile-admin-secret"), "导入文本 profile 未恢复管理 access key");
|
||||
const importedTextSecretPath = path.join(tempRoot, ".codex-retry-gateway", "secrets", "imported-text.token");
|
||||
assert(
|
||||
(await readFile(importedTextSecretPath, "utf8")).trim()
|
||||
=== "text-export-secret",
|
||||
"导入文本 profile 未把 API key 写入本机 secret 文件",
|
||||
);
|
||||
assert(((await stat(importedTextSecretPath)).mode & 0o777) === 0o600, "导入文本 profile secret 权限不是 0600");
|
||||
|
||||
const duplicateTextImportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles/import`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ bundle: textExportBundle, name: "imported-text" }),
|
||||
},
|
||||
);
|
||||
assert(duplicateTextImportResponse.status === 409, "同名文本 profile 未确认覆盖时不应导入");
|
||||
const duplicateTextImportPayload = await duplicateTextImportResponse.json();
|
||||
assert(duplicateTextImportPayload?.error?.code === "profile_import_exists", "同名文本 profile 未返回冲突标识");
|
||||
|
||||
const activeTextImportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/profiles/import`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ bundle: textExportBundle, name: "legacy-text", overwrite: true }),
|
||||
},
|
||||
);
|
||||
assert(activeTextImportResponse.status === 409, "不应允许导入覆盖当前文本 profile");
|
||||
const activeTextImportPayload = await activeTextImportResponse.json();
|
||||
assert(activeTextImportPayload?.error?.code === "profile_import_active_conflict", "当前文本 profile 冲突标识错误");
|
||||
|
||||
const imageProfileResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/image-profiles`,
|
||||
{
|
||||
@@ -822,6 +950,61 @@ async function run() {
|
||||
assert(imagePrimary?.summary?.auth_source === "system secret file configured", "独立图片 profile 未返回认证来源");
|
||||
assert(!JSON.stringify(imageProfilePayload).includes("test-image-profile-secret"), "图片 profile API 不应返回图片明文 secret");
|
||||
|
||||
const wrongImageExportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/image-profiles/export`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ profile: "image-primary", verification_key: "wrong-image-secret" }),
|
||||
},
|
||||
);
|
||||
assert(wrongImageExportResponse.status === 403, "错误图片 key 应拒绝导出");
|
||||
const wrongImageExportPayload = await wrongImageExportResponse.json();
|
||||
assert(wrongImageExportPayload?.error?.code === "profile_export_key_invalid", "错误图片 key 未返回验证失败标识");
|
||||
assert(!JSON.stringify(wrongImageExportPayload).includes("test-image-profile-secret"), "错误图片 key 响应泄露了真实 key");
|
||||
|
||||
const imageExportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/image-profiles/export`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ profile: "image-primary", verification_key: "test-image-profile-secret" }),
|
||||
},
|
||||
);
|
||||
assert(imageExportResponse.status === 200, `图片 profile 导出失败: ${imageExportResponse.status}`);
|
||||
assert((imageExportResponse.headers.get("cache-control") || "").includes("no-store"), "图片导出响应未禁用缓存");
|
||||
const imageExportBundle = await imageExportResponse.json();
|
||||
assert(imageExportBundle?.kind === "image", "图片导出 kind 错误");
|
||||
assert(imageExportBundle?.profile?.key === "test-image-profile-secret", "图片导出文件未包含 API key");
|
||||
assert(imageExportBundle?.profile?.auth_mode === "manual_bearer", "图片导出文件未转换为可移植认证模式");
|
||||
|
||||
const imageImportResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/image-profiles/import`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...adminHeaders, "content-type": "application/json" },
|
||||
body: JSON.stringify({ bundle: imageExportBundle, name: "imported-image" }),
|
||||
},
|
||||
);
|
||||
assert(imageImportResponse.status === 200, `图片 profile 导入失败: ${imageImportResponse.status}`);
|
||||
const imageImportPayload = await imageImportResponse.json();
|
||||
const importedImageProfile = (imageImportPayload.image_profiles || []).find((profile) => profile?.name === "imported-image");
|
||||
assert(importedImageProfile?.form?.auth_mode === "manual_bearer", "导入图片 profile 未使用 manual_bearer");
|
||||
assert(importedImageProfile?.form?.manual_secret_configured === true, "导入图片 profile 未配置本机 secret");
|
||||
assert(!JSON.stringify(imageImportPayload).includes("test-image-profile-secret"), "图片导入响应泄露了 API key");
|
||||
const importedImageEnv = await readFile(path.join(imageProfilesDir, "imported-image.env"), "utf8");
|
||||
assert(!importedImageEnv.includes("test-image-profile-secret"), "导入图片 profile env 不应包含明文 API key");
|
||||
const importedImageSecretPath = path.join(tempRoot, ".codex-retry-gateway", "secrets", "imported-image.images.token");
|
||||
assert(
|
||||
(await readFile(importedImageSecretPath, "utf8")).trim()
|
||||
=== "test-image-profile-secret",
|
||||
"导入图片 profile 未把 API key 写入本机 secret 文件",
|
||||
);
|
||||
assert(((await stat(importedImageSecretPath)).mode & 0o777) === 0o600, "导入图片 profile secret 权限不是 0600");
|
||||
const profileTransferLog = await readFile(logPath, "utf8");
|
||||
assert(!profileTransferLog.includes("text-export-secret"), "gateway 日志泄露了文本 profile 导出 key");
|
||||
assert(!profileTransferLog.includes("test-image-profile-secret"), "gateway 日志泄露了图片 profile 导出 key");
|
||||
|
||||
const switchImageProfileResponse = await fetch(
|
||||
`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/image-profiles/switch`,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user