CI / build-and-test (push) Successful in 14m43s
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.
97 lines
4.2 KiB
JavaScript
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;
|
|
}
|