videoscriber-backend / lib /hf-bridge.js
Rimas Kavaliauskas
Switch Space to Docker backend and sync Videoscriber post-processing
6782be3
import crypto from 'crypto';
import { getConfig } from './config.js';
function normalizeBaseUrl(baseUrl) {
return String(baseUrl || '').trim().replace(/\/+$/, '');
}
function getHeader(headers, name) {
const key = String(name || '').toLowerCase();
if (!headers || typeof headers !== 'object') return '';
for (const [headerName, value] of Object.entries(headers)) {
if (String(headerName || '').toLowerCase() === key) {
return String(value || '').trim();
}
}
return '';
}
function safeJsonParse(text) {
try {
return JSON.parse(String(text || ''));
} catch {
return null;
}
}
function toCanonicalJson(value) {
if (Array.isArray(value)) {
return `[${value.map((item) => toCanonicalJson(item)).join(',')}]`;
}
if (value && typeof value === 'object') {
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${toCanonicalJson(item)}`).join(',')}}`;
}
return JSON.stringify(value);
}
export function createHfSignature(secret, timestamp, payload) {
const serialized = toCanonicalJson(payload || {});
return crypto
.createHmac('sha256', String(secret || ''))
.update(`${String(timestamp || '')}.${serialized}`)
.digest('hex');
}
export function buildHfPostTranscribeUrl(baseUrl) {
const normalized = normalizeBaseUrl(baseUrl);
if (!normalized) return '';
return `${normalized}/api/hf/post-transcribe`;
}
export function verifyHfIncomingRequest({ headers, body }) {
const cfg = getConfig();
const configuredSecret = String(cfg.hfSharedSecret || '').trim();
if (!configuredSecret) {
return { ok: false, reason: 'hf_shared_secret_not_configured' };
}
const incomingSecret = getHeader(headers, 'x-hf-shared-secret');
if (!incomingSecret || incomingSecret !== configuredSecret) {
return { ok: false, reason: 'hf_shared_secret_mismatch' };
}
const timestamp = getHeader(headers, 'x-hf-timestamp');
const signature = getHeader(headers, 'x-hf-signature');
if (!timestamp || !signature) {
return { ok: false, reason: 'hf_signature_missing' };
}
const timestampNum = Number(timestamp);
if (!Number.isFinite(timestampNum)) {
return { ok: false, reason: 'hf_timestamp_invalid' };
}
const driftMs = Math.abs(Date.now() - timestampNum);
if (driftMs > 5 * 60 * 1000) {
return { ok: false, reason: 'hf_timestamp_out_of_window' };
}
const expectedSignature = createHfSignature(configuredSecret, timestamp, body || {});
if (signature !== expectedSignature) {
return { ok: false, reason: 'hf_signature_mismatch' };
}
return { ok: true };
}
// Vercel-side helper:
// after /api/transcribe succeeds, payload is forwarded to HF backend for Atlas persistence + post-processing.
export async function forwardTranscriptToHf(payload, { timeoutMs = 4000 } = {}) {
const cfg = getConfig();
const endpoint = buildHfPostTranscribeUrl(cfg.hfBackendBaseUrl);
const secret = String(cfg.hfSharedSecret || '').trim();
if (!endpoint || !secret) {
return {
ok: false,
skipped: true,
reason: 'hf_bridge_not_configured',
};
}
const timestamp = String(Date.now());
const signature = createHfSignature(secret, timestamp, payload || {});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-HF-Shared-Secret': secret,
'X-HF-Timestamp': timestamp,
'X-HF-Signature': signature,
},
body: JSON.stringify(payload || {}),
signal: controller.signal,
});
const text = await response.text();
const data = safeJsonParse(text) || { raw: text };
return {
ok: response.ok,
status: response.status,
data,
skipped: false,
};
} catch (error) {
return {
ok: false,
skipped: false,
reason: error?.name === 'AbortError' ? 'hf_forward_timeout' : 'hf_forward_error',
error: String(error?.message || error),
};
} finally {
clearTimeout(timeout);
}
}