| 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 }; |
| } |
|
|
| |
| |
| 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); |
| } |
| } |
|
|