type DurationDefaultUnit = 'ms' | 's';
const INLINE_DOC2X_API_KEY = '';
function parseDoc2xApiKey(raw: string): string {
const v = String(raw).trim();
if (!v) return '';
// Common misconfig: Alma/Codex passes literal "${DOC2X_API_KEY}" without expansion.
if (v.includes('${') && v.includes('}')) return '';
const bearerPrefix = /^bearer\s+/i;
if (bearerPrefix.test(v)) return v.replace(bearerPrefix, '').trim();
return v;
}
function resolveApiKey(): { apiKey: string; source: 'inline' | 'env' | 'missing' } {
const inline = parseDoc2xApiKey(INLINE_DOC2X_API_KEY);
if (inline) return { apiKey: inline, source: 'inline' };
const env = parseDoc2xApiKey(process.env.DOC2X_API_KEY || '');
if (env) return { apiKey: env, source: 'env' };
return { apiKey: '', source: 'missing' };
}
function getEnvInt(name: string, def: number): number {
const raw = process.env[name];
if (raw == null || String(raw).trim() === '') return def;
const n = Number(raw);
if (!Number.isFinite(n) || n <= 0) throw new Error(`Invalid env ${name}: ${raw}`);
return Math.floor(n);
}
function getEnvNonNegativeInt(name: string, def: number): number {
const raw = process.env[name];
if (raw == null || String(raw).trim() === '') return def;
const n = Number(raw);
if (!Number.isFinite(n) || n < 0) throw new Error(`Invalid env ${name}: ${raw}`);
return Math.floor(n);
}
function parsePositiveDurationMs(raw: string, defaultUnit: DurationDefaultUnit): number {
const v = String(raw).trim().toLowerCase();
if (!v) throw new Error('empty duration');
const m = v.match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/);
if (!m) throw new Error(`invalid duration: ${raw}`);
const num = Number(m[1]);
if (!Number.isFinite(num) || num <= 0) throw new Error(`invalid duration: ${raw}`);
const unit = (m[2] as 'ms' | 's' | 'm' | undefined) ?? defaultUnit;
const ms = unit === 'ms' ? num : unit === 's' ? num * 1000 : num * 60_000;
if (!Number.isFinite(ms) || ms <= 0) throw new Error(`invalid duration: ${raw}`);
return Math.floor(ms);
}
function resolveHttpTimeoutMs(): number {
const msRaw = process.env.DOC2X_HTTP_TIMEOUT_MS;
if (msRaw != null && String(msRaw).trim() !== '') return parsePositiveDurationMs(msRaw, 'ms');
const raw = process.env.DOC2X_HTTP_TIMEOUT;
if (raw != null && String(raw).trim() !== '') return parsePositiveDurationMs(raw, 's');
return 60_000;
}
export const RESOLVED_KEY = resolveApiKey();
export const CONFIG = Object.freeze({
baseUrl: (process.env.DOC2X_BASE_URL || 'https://v2.doc2x.noedgeai.com').replace(/\/+$/, ''),
apiKey: RESOLVED_KEY.apiKey,
httpTimeoutMs: resolveHttpTimeoutMs(),
pollIntervalMs: getEnvInt('DOC2X_POLL_INTERVAL_MS', 2_000),
maxWaitMs: getEnvInt('DOC2X_MAX_WAIT_MS', 600_000),
parsePdfMaxOutputChars: getEnvNonNegativeInt('DOC2X_PARSE_PDF_MAX_OUTPUT_CHARS', 5_000),
parsePdfMaxOutputPages: getEnvNonNegativeInt('DOC2X_PARSE_PDF_MAX_OUTPUT_PAGES', 10),
});
const DEFAULT_DOWNLOAD_HOST_SUFFIX_ALLOWLIST = Object.freeze([
'.amazonaws.com.cn',
'.aliyuncs.com',
'.noedgeai.com',
]);
export function parseDownloadUrlAllowlist(): string[] {
const raw = String(process.env.DOC2X_DOWNLOAD_URL_ALLOWLIST || '').trim();
if (!raw) return [...DEFAULT_DOWNLOAD_HOST_SUFFIX_ALLOWLIST];
if (raw === '*') return ['*'];
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}
export function isHostAllowedByAllowlist(hostname: string, allowlist: string[]): boolean {
const host = hostname.toLowerCase();
for (const rule of allowlist) {
const r = rule.toLowerCase();
if (r === '*') return true;
if (r.startsWith('.')) {
if (host.endsWith(r)) return true;
continue;
}
if (host === r) return true;
if (host.endsWith('.' + r)) return true;
}
return false;
}