Files
openmaic/OpenMAIC/app/api/verify-pdf-provider/route.ts
2026-08-16 14:58:47 +08:00

208 lines
7.5 KiB
TypeScript

import { NextRequest } from 'next/server';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess } from '@/lib/server/api-response';
import {
isServerConfiguredProvider,
resolveManagedAliDocMindCredentials,
resolvePDFApiKey,
resolvePDFBaseUrl,
} from '@/lib/server/provider-config';
import { validateUrlForSSRF } from '@/lib/server/ssrf-guard';
import { MINERU_CLOUD_DEFAULT_BASE } from '@/lib/pdf/constants';
import { fetchPinnedPublicUrl, PublicUrlFetchError } from '@/lib/server/public-url-fetch';
const log = createLogger('Verify PDF Provider');
export const runtime = 'nodejs';
function publicCloudFetchError(error: unknown) {
if (
error instanceof PublicUrlFetchError &&
(error.code === 'INVALID_URL' ||
error.code === 'BLOCKED_TARGET' ||
error.code === 'DNS_FAILURE')
) {
return apiError('INVALID_URL', 403, 'Cloud endpoint is not an allowed public URL');
}
return apiError('UPSTREAM_ERROR', 502, 'Unable to connect to the cloud provider');
}
export async function POST(req: NextRequest) {
let providerId: string | undefined;
try {
const body = await req.json();
providerId = body.providerId;
const { apiKey, baseUrl, accessKeyId, accessKeySecret } = body;
if (!providerId) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'Provider ID is required');
}
// Managed providers are admin-owned: ignore any client-sent key/baseUrl.
const managed = isServerConfiguredProvider('pdf', providerId);
// AliDocMind: verify AK/SK by issuing a lightweight authenticated probe.
if (providerId === 'alidocmind') {
let ak: string | undefined;
let sk: string | undefined;
let endpoint: string | undefined;
if (managed) {
// Managed: use server-owned credentials + endpoint only. Ignore any
// client-supplied AK/SK/baseUrl.
const serverCreds = resolveManagedAliDocMindCredentials();
if (!serverCreds) {
return apiError('INTERNAL_ERROR', 500, 'AliDocMind is not configured on the server');
}
ak = serverCreds.accessKeyId;
sk = serverCreds.accessKeySecret;
endpoint = serverCreds.baseUrl;
} else {
// Unmanaged: client credentials only — never fall back to server env.
ak = (accessKeyId as string | undefined) || undefined;
sk = (accessKeySecret as string | undefined) || undefined;
endpoint = (baseUrl as string | undefined) || undefined;
if (!ak || !sk) {
return apiError(
'MISSING_REQUIRED_FIELD',
400,
'AccessKey ID and AccessKey Secret are required for AliDocMind',
);
}
// Validate a client-supplied endpoint before we sign a request to it.
if (endpoint && process.env.NODE_ENV === 'production') {
const ssrfError = await validateUrlForSSRF(
endpoint.startsWith('http') ? endpoint : `https://${endpoint}`,
);
if (ssrfError) {
return apiError('INVALID_URL', 403, ssrfError);
}
}
}
const { verifyAliDocMindCredentials } = await import('@/lib/pdf/alidocmind-client');
const result = await verifyAliDocMindCredentials({
accessKeyId: ak,
accessKeySecret: sk,
endpoint,
});
if (!result.ok) {
return apiError('INVALID_CREDENTIALS', 400, `Authentication failed: ${result.error}`);
}
return apiSuccess({ message: 'Connection successful' });
}
// MinerU Cloud: verify by calling the cloud API with the token
if (providerId === 'mineru-cloud') {
const clientCloudBase = managed ? undefined : (baseUrl as string | undefined) || undefined;
const resolvedApiKey = resolvePDFApiKey(providerId, managed ? undefined : apiKey);
if (!resolvedApiKey) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'API Key is required for MinerU Cloud');
}
const cloudBase = (
resolvePDFBaseUrl(providerId, clientCloudBase) || MINERU_CLOUD_DEFAULT_BASE
).replace(/\/+$/, '');
// Probe through a DNS-pinned, public-only connection. The bearer token is
// never forwarded across a redirect because redirects fail closed.
let pinnedResponse;
try {
pinnedResponse = await fetchPinnedPublicUrl(
`${cloudBase}/extract-results/batch/test-connection`,
{
headers: {
Authorization: `Bearer ${resolvedApiKey}`,
Accept: 'application/json',
},
signal: AbortSignal.timeout(10_000),
maxResponseBytes: 1024 * 1024,
},
);
} catch (error) {
return publicCloudFetchError(error);
}
const { response } = pinnedResponse;
try {
if (response.status >= 300 && response.status < 400) {
return apiError('REDIRECT_NOT_ALLOWED', 403, 'Redirects are not allowed');
}
// Other responses (including 4xx for "batch not found") mean auth + connectivity works.
// Only network errors, redirects, or 401/403 indicate a problem.
if (response.status === 401 || response.status === 403) {
const text = await response.text().catch(() => '');
return apiError(
'INTERNAL_ERROR',
500,
`Authentication failed: ${text || response.statusText}`,
);
}
return apiSuccess({
message: 'Connection successful',
status: response.status,
});
} finally {
await pinnedResponse.dispose();
}
}
// Self-hosted providers: verify by connecting to the base URL
const clientBaseUrl = managed ? undefined : (baseUrl as string | undefined) || undefined;
if (clientBaseUrl && process.env.NODE_ENV === 'production') {
const ssrfError = await validateUrlForSSRF(clientBaseUrl);
if (ssrfError) {
return apiError('INVALID_URL', 403, ssrfError);
}
}
const resolvedBaseUrl = resolvePDFBaseUrl(providerId, clientBaseUrl);
if (!resolvedBaseUrl) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'Base URL is required');
}
const resolvedApiKey = resolvePDFApiKey(providerId, managed ? undefined : apiKey);
const headers: Record<string, string> = {};
if (resolvedApiKey) {
headers['Authorization'] = `Bearer ${resolvedApiKey}`;
}
const response = await fetch(resolvedBaseUrl, {
headers,
signal: AbortSignal.timeout(10000),
redirect: 'manual',
});
if (response.status >= 300 && response.status < 400) {
return apiError('REDIRECT_NOT_ALLOWED', 403, 'Redirects are not allowed');
}
// MinerU's FastAPI root returns 404 (no root route), but the server is reachable.
// Any HTTP response (including 404) means the server is up.
return apiSuccess({
message: 'Connection successful',
status: response.status,
});
} catch (error) {
log.error(`PDF provider verification failed [provider=${providerId ?? 'unknown'}]:`, error);
let errorMessage = 'Connection failed';
if (error instanceof Error) {
if (error.message.includes('ECONNREFUSED')) {
errorMessage = 'Cannot connect to server, please check the Base URL';
} else if (error.message.includes('ENOTFOUND')) {
errorMessage = 'Server not found, please check the Base URL';
} else if (error.message.includes('timeout') || error.name === 'TimeoutError') {
errorMessage = 'Connection timed out';
} else {
errorMessage = error.message;
}
}
return apiError('INTERNAL_ERROR', 500, errorMessage);
}
}