feat: enforce api production safety limits

This commit is contained in:
Codex
2026-06-28 21:20:38 +08:00
parent a8e0ac78be
commit 1d873b2e50
8 changed files with 177 additions and 14 deletions

View File

@@ -109,6 +109,8 @@ async function startServerIfNeeded() {
...process.env,
PORT: String(port),
DATABASE_URL: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
MAX_JSON_BODY_BYTES: process.env.MAX_JSON_BODY_BYTES || '8192',
MAX_IMPORT_JSON_BODY_BYTES: process.env.MAX_IMPORT_JSON_BODY_BYTES || '65536',
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
@@ -124,6 +126,56 @@ async function startServerIfNeeded() {
await waitForHealth();
}
async function waitForProcessExit(child, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
child.kill();
reject(new Error('process did not exit before timeout'));
}, timeoutMs);
child.on('exit', (code, signal) => {
clearTimeout(timer);
resolve({ code, signal });
});
child.on('error', error => {
clearTimeout(timer);
reject(error);
});
});
}
async function testProductionConfigFailFast() {
const port = await getFreePort();
const child = spawn(process.execPath, ['apps/api/dist/apps/api/src/server.js'], {
cwd: process.cwd(),
env: {
...process.env,
NODE_ENV: 'production',
PORT: String(port),
DATABASE_URL: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
CORS_ORIGIN: '*',
AUTH_SMS_PROVIDER: 'mock',
AUTH_CODE_PEPPER: 'development-code-pepper-change-me',
AUTH_SESSION_SECRET: 'development-session-secret-change-me',
PLATFORM_ADMIN_API_KEY: 'local-platform-admin-key',
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
let logs = '';
child.stdout.on('data', chunk => {
logs += chunk.toString();
});
child.stderr.on('data', chunk => {
logs += chunk.toString();
});
const result = await waitForProcessExit(child);
assert.notEqual(result.code, 0, 'production server with unsafe defaults should fail to start');
assert.match(logs, /Invalid production API configuration/, 'production fail-fast should explain unsafe config');
}
function stopServer() {
if (serverProcess && !serverProcess.killed) {
serverProcess.kill();
@@ -818,6 +870,38 @@ async function testTenantContentAssetsAndImports() {
});
assert.equal(deniedImport.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not preview content import');
const oversizedNormalJson = await request('/api/auth/sms/send', {
method: 'POST',
body: {
phone: '13800000009',
purpose: 'login',
padding: 'x'.repeat(9000),
},
expectStatus: 413,
});
assert.equal(oversizedNormalJson.code, 'JSON_BODY_TOO_LARGE', 'ordinary JSON endpoints should enforce body size limit');
const largeImportPreview = await request('/api/tenant-content/imports/preview/questions', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
sourceName: 'large-but-allowed-question-import.json',
subjectId: ids.subject,
categoryId: ids.category,
regionId: ids.region,
items: [
{
legacyId: 'integration-large-preview-001',
type: 'choice',
content: `大体积导入预览:${'企业级导入需要受控上限。'.repeat(500)}`,
options: ['正确', '错误'],
correctOptionIndices: [0],
},
],
},
});
assert.equal(largeImportPreview.job?.errorCount, 0, 'import preview should use larger bounded body limit');
const invalidPreview = await request('/api/tenant-content/imports/preview/questions', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
@@ -1680,6 +1764,7 @@ async function testReferralAndCrmGrowth() {
async function main() {
try {
await check('production config fail-fast', testProductionConfigFailFast);
await startServerIfNeeded();
console.log(`[INFO] API integration target: ${apiBase}`);