SPB Git forge

spb/trouve-ka

Public ★ Pinned

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

40commits 1branches 0releases
9.2 MBsize
maindefault branch
23 days agolast push
Python 51.9% TypeScript 32.7% JavaScript 4.8% CSS 4.1% Shell 2.9% HTML 1.8% SQL 1.5%
4.5 KB · 143 lines typescript
Raw Blame History
1/**2 * Trouve-KA — favoris « Mon univers Ka » : client serveur du hub Groupe KA3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 *6 * Le hub groupe-ka.com est le MAGASIN CENTRAL des favoris du groupe :7 * Trouve-KA ne stocke rien localement. Chaque ♥ est poussé au hub (POST8 * signé HMAC-SHA256, synchrone : un échec remonte à l'appelant) et la liste9 * est lue au hub (GET signé, cache mémoire 30 s, invalidé à chaque toggle).10 * Même secret que le SSO (KA_SSO_SECRET). Appels SERVEUR uniquement.11 * Modèle de référence : food-ka/foodka/hubfav.py.12 */13import { createHash, createHmac } from "node:crypto";14import { CLIENT_ID, KA_HUB_URL } from "@/lib/ka-auth";1516const CACHE_TTL_MS = 30_000; // liste des favoris17const TIMEOUT_MS = 6_000;1819/** Un favori tel que stocké au hub. */20export type HubFavItem = {21  item_id: string;22  title: string;23  subtitle?: string;24  price_label?: string;25  image_url?: string;26  url?: string;27  meta?: Record<string, unknown>;28};2930/** Champs d'item acceptés -> longueur maximale (troncature défensive). */31const FIELD_MAX: Record<string, number> = {32  item_id: 120,33  title: 200,34  subtitle: 200,35  price_label: 60,36  image_url: 500,37  url: 500,38};3940const cache = new Map<string, { at: number; favs: HubFavItem[] }>();4142/** Signature HMAC-SHA256 du hub : hex("trouve-ka.<ka_id>.<ts>"). */43function sig(kaId: string, ts: number): string | null {44  const secret = process.env.KA_SSO_SECRET;45  if (!secret) return null;46  return createHmac("sha256", secret)47    .update(`${CLIENT_ID}.${kaId}.${ts}`)48    .digest("hex");49}5051/** Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe). */52export function linked(kaId: string | null | undefined): boolean {53  return Boolean(kaId) && String(kaId).startsWith("ka-");54}5556/** Identifiant STABLE d'un résultat : dérivé de son URL (les résultats de57 *  Trouve-KA sont des pages — l'URL est leur identité). */58export function itemIdForUrl(url: string): string {59  return `pg-${createHash("sha256").update(url).digest("hex").slice(0, 40)}`;60}6162/** Ne garde que les champs d'item connus, en chaînes tronquées. */63export function cleanItem(item: Record<string, unknown>): Partial<HubFavItem> {64  const out: Record<string, string> = {};65  for (const [k, max] of Object.entries(FIELD_MAX)) {66    const v = item?.[k];67    if (v !== undefined && v !== null && v !== "") {68      out[k] = String(v).slice(0, max);69    }70  }71  return out as Partial<HubFavItem>;72}7374/** Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :75 *  le hub est le magasin des favoris, l'échec doit remonter à l'appelant. */76export async function hubToggle(77  kaId: string,78  action: "add" | "remove",79  item: Partial<HubFavItem>,80): Promise<boolean> {81  const ts = Math.floor(Date.now() / 1000);82  const s = sig(kaId, ts);83  if (!s || !linked(kaId) || (action !== "add" && action !== "remove")) {84    return false;85  }86  let ok = false;87  try {88    const r = await fetch(`${KA_HUB_URL}/api/sso/favorites`, {89      method: "POST",90      headers: { "Content-Type": "application/json" },91      body: JSON.stringify({92        client_id: CLIENT_ID,93        ka_id: kaId,94        ts: String(ts),95        sig: s,96        action,97        item,98      }),99      signal: AbortSignal.timeout(TIMEOUT_MS),100      cache: "no-store",101    });102    ok = r.status === 200;103  } catch {104    ok = false;105  }106  if (ok) cache.delete(kaId); // la prochaine lecture reflète le toggle107  return ok;108}109110/** Favoris Trouve-KA du membre, lus au hub (cache mémoire 30 s).111 *  [] = aucun favori ; null = hub injoignable (erreur, jamais mise en cache). */112export async function hubList(kaId: string): Promise<HubFavItem[] | null> {113  if (!linked(kaId)) return []; // compte legacy non relié au hub114  const now = Date.now();115  const hit = cache.get(kaId);116  if (hit && now - hit.at < CACHE_TTL_MS) return hit.favs;117  const ts = Math.floor(now / 1000);118  const s = sig(kaId, ts);119  if (!s) return null;120  try {121    const params = new URLSearchParams({122      client_id: CLIENT_ID,123      ka_id: kaId,124      ts: String(ts),125      sig: s,126    });127    const r = await fetch(`${KA_HUB_URL}/api/sso/favorites?${params}`, {128      signal: AbortSignal.timeout(TIMEOUT_MS),129      cache: "no-store",130    });131    if (r.status !== 200) return null;132    const data = (await r.json()) as { favorites?: unknown };133    if (!Array.isArray(data.favorites)) return null;134    const favs = data.favorites.filter(135      (f): f is HubFavItem => typeof f === "object" && f !== null,136    );137    cache.set(kaId, { at: now, favs });138    return favs;139  } catch {140    return null;141  }142}143