Files
NewTube/server/search-transport.mjs
T
bruno 665a0f0ebd
CI / build-and-test (push) Successful in 14m43s
feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
2026-09-30 07:57:11 -04:00

97 lines
4.2 KiB
JavaScript

/**
* Phase 7.3 / 7.6 — transport de `/api/search`.
*
* Deux besoins nés dans la même route, regroupés parce que ce sont DEUX TRANSPORTS
* du même contrat côté serveur :
* - Phase 7.3 : le payload de diagnostic `?debug=1` (provenance + `raw` tronqué) ;
* - Phase 7.6 : le media type `application/x-ndjson` du flux incrémental.
*
* Isolé dans son propre module (et non dans `index.mjs`) pour deux raisons :
* - `index.mjs` fait 4 200+ lignes et démarre un serveur à l'import : la logique
* de diagnostic ne peut donc pas être testée depuis le fichier d'entrée ;
* - la troncature et la redaction sont une BARRIÈRE DE SÉCURITÉ. Elles
* s'évaporent à la première refonte si elles vivent au milieu des routes.
*
* Chaîne du debug volontairement bornée : le but est de voir ce que le provider
* a renvoyé et ce qui a survécu au mapping (pour comprendre un `views` manquant),
* pas de transporter une réponse entière. Garde-fous : 2 items par provider,
* 600 caractères de JSON, et suppression des clés d'habilitation.
*/
export const DEBUG_RAW_MAX_CHARS = 600;
export const DEBUG_RAW_ITEMS_PER_PROVIDER = 2;
/**
* Phase 7.6 — media type du transport incrémental de `/api/search`.
*
* Exporté (et non écrit en littéral dans la route) parce que le client doit
* négocier le MÊME type : un désaccord d'une lettre ferait retomber le front
* silencieusement en mode atomique, sans le moindre message d'erreur — le pire
* scénario possible pour une optimization de performance.
*/
export const APPLICATION_NDJSON = 'application/x-ndjson';
/**
* Clés d'habilitation retirées du `raw`. Volontairement large : un `raw`
* publié par erreur est journalisé en CI, donc en clair dans les artefacts de
* build. Un faux positif coûte un `[redacted]` de plus dans un payload de debug.
*/
const DEBUG_SECRET_KEYS = /^(api[-_]?key|apikey|authorization|auth|token|access[-_]?token|refresh[-_]?token|cookie|set[-_]?cookie|password|passwd|secret|client[-_]?secret|private[-_]?key|signature)$/i;
/** Copie profonde superficiellement, valeurs sensibles remplacées. */
export function redactDebugSecrets(value) {
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(redactDebugSecrets);
const out = {};
for (const [k, v] of Object.entries(value)) {
out[k] = DEBUG_SECRET_KEYS.test(k) ? '[redacted]' : redactDebugSecrets(v);
}
return out;
}
/**
* @param {Record<string, any[]>} groups groupes de résultats par provider
* @param {Record<string, {message?: string}>} [errors]
*/
export function buildDebugPayload(groups, errors) {
const out = {};
for (const [pid, items] of Object.entries(groups || {})) {
const list = Array.isArray(items) ? items : [];
const sample = list.slice(0, DEBUG_RAW_ITEMS_PER_PROVIDER).map((item) => {
if (!item || typeof item !== 'object') return { value: item };
const { raw, ...rest } = item;
const base = {
id: rest.id ?? null,
source: rest.source ?? null,
capturedAt: rest.capturedAt ?? null,
// Les NOMS des champs mappés, pas seulement leur nombre : c'est ce qui
// permet de distinguer « `views` absent » de « `views` nul ». Triés pour
// que deux debugs successifs soient comparables.
fields: Object.keys(rest)
.filter((k) => k !== 'id' && k !== 'source' && k !== 'capturedAt')
.sort(),
};
// `raw` n'est aujourd'hui produit par aucun adaptateur (ils normalisent
// sans conserver la charge utile amont) : la branche reste pour le jour où
// l'un d'eux l'expose, et `fields` prend le relais en attendant.
if (raw === undefined) return base;
let json;
try { json = JSON.stringify(redactDebugSecrets(raw)); } catch { json = '[unserialisable]'; }
return {
...base,
raw: json.length > DEBUG_RAW_MAX_CHARS
? `${json.slice(0, DEBUG_RAW_MAX_CHARS)}… (${json.length} car.)`
: json,
};
});
out[pid] = {
count: list.length,
error: errors?.[pid]?.message ?? null,
capturedAt: list.find((i) => i && i.capturedAt)?.capturedAt ?? null,
source: list.find((i) => i && i.source)?.source ?? null,
items: sample,
};
}
return out;
}