feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
CI / build-and-test (push) Successful in 14m43s
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.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user