SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
5.6 KB · 150 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// -----------------------------------------------------------------------------3// Favoris « Mon univers Ka » — le hub Groupe KA (groupe-ka.com) est le MAGASIN4// CENTRAL des favoris du groupe : Vrai-Prix ne stocke RIEN localement.5//   · Chaque ♥ est poussé au hub (POST signé HMAC, SYNCHRONE : un échec6//     remonte à l'appelant) ; la liste est lue au hub (GET signé, cache7//     mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.8//   · sig = HMAC-SHA256(KA_SSO_SECRET, "vrai-prix.<ka_id>.<ts>") en hex,9//     ts epoch en secondes (fenêtre ±300 s côté hub).10//   · Appels SERVEUR uniquement (route handlers) — jamais depuis le navigateur.11//   Aucune dépendance : node:crypto + fetch natif seulement.12//   Config .env.local : KA_SSO_SECRET, KA_HUB_URL (optionnel).13// -----------------------------------------------------------------------------14import { createHmac } from "node:crypto";15import { CLIENT_ID, KA_HUB_URL } from "@/lib/ka-auth";1617const CACHE_TTL_MS = 30_000; // 30 s — liste des favoris18const TIMEOUT_MS = 6_000; // 6 s1920/** Item de favori tel qu'accepté / rendu par le hub. */21export type FavItem = {22  item_id: string;23  title: string;24  subtitle?: string;25  price_label?: string;26  image_url?: string;27  url?: string;28  meta?: Record<string, unknown>;29};3031// champs d'item acceptés → longueur maximale (troncature défensive)32const FIELDS: { key: Exclude<keyof FavItem, "meta">; max: number }[] = [33  { key: "item_id", max: 120 },34  { key: "title", max: 200 },35  { key: "subtitle", max: 200 },36  { key: "price_label", max: 60 },37  { key: "image_url", max: 500 },38  { key: "url", max: 500 },39];4041/** Signature HMAC-SHA256 du hub : hex("vrai-prix.<ka_id>.<ts>"). */42function sig(kaId: string, ts: number): string | null {43  const secret = process.env.KA_SSO_SECRET;44  if (!secret) return null;45  return createHmac("sha256", secret)46    .update(`${CLIENT_ID}.${kaId}.${ts}`)47    .digest("hex");48}4950/** Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe). */51export function linked(kaId: string | null | undefined): boolean {52  return !!kaId && String(kaId).startsWith("ka-");53}5455/** Ne garde que les champs d'item connus (chaînes tronquées) — null si pas56 *  d'item_id exploitable. `title` retombe sur l'item_id. */57export function cleanItem(raw: unknown): FavItem | null {58  if (typeof raw !== "object" || raw === null) return null;59  const src = raw as Record<string, unknown>;60  const out: Record<string, unknown> = {};61  for (const { key, max } of FIELDS) {62    const v = src[key];63    if (v == null || v === "") continue;64    out[key] = String(v).slice(0, max);65  }66  if (typeof out.item_id !== "string" || !out.item_id) return null;67  if (typeof out.title !== "string" || !out.title) out.title = out.item_id;68  if (src.meta && typeof src.meta === "object" && !Array.isArray(src.meta)) {69    out.meta = src.meta as Record<string, unknown>;70  }71  return out as FavItem;72}7374// Cache mémoire process-wide (Map globale — survit au HMR en dev).75type CacheEntry = { at: number; favs: FavItem[] };76const g = globalThis as unknown as { __vpHubFavCache?: Map<string, CacheEntry> };77const cache: Map<string, CacheEntry> =78  g.__vpHubFavCache ?? (g.__vpHubFavCache = new Map());7980/** fetch avec délai maximal (AbortController) — null sur toute erreur réseau. */81async function hubFetch(url: string, init?: RequestInit): Promise<Response | null> {82  const ctrl = new AbortController();83  const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);84  try {85    return await fetch(url, { ...init, signal: ctrl.signal, cache: "no-store" });86  } catch {87    return null;88  } finally {89    clearTimeout(timer);90  }91}9293/** Favoris Vrai-Prix du membre, lus au hub (cache mémoire 30 s).94 *  [] = aucun favori ; null = hub injoignable (erreur, jamais mise en cache). */95export async function hubList(kaId: string): Promise<FavItem[] | null> {96  if (!linked(kaId)) return []; // compte non relié au hub97  const now = Date.now();98  const hit = cache.get(kaId);99  if (hit && now - hit.at < CACHE_TTL_MS) return hit.favs;100101  const ts = Math.floor(now / 1000);102  const s = sig(kaId, ts);103  if (!s) return null; // KA_SSO_SECRET absent104  const url =105    `${KA_HUB_URL}/api/sso/favorites?client_id=${encodeURIComponent(CLIENT_ID)}` +106    `&ka_id=${encodeURIComponent(kaId)}&ts=${ts}&sig=${s}`;107  const r = await hubFetch(url);108  if (!r || !r.ok) return null;109  let favs: unknown;110  try {111    favs = ((await r.json()) as { favorites?: unknown })?.favorites ?? [];112  } catch {113    return null;114  }115  if (!Array.isArray(favs)) return null;116  const clean = favs.filter(117    (f): f is FavItem =>118      typeof f === "object" && f !== null && typeof (f as FavItem).item_id === "string",119  );120  cache.set(kaId, { at: now, favs: clean });121  return clean;122}123124/** Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :125 *  le hub est le magasin des favoris, l'échec doit remonter à l'appelant. */126export async function hubToggle(127  kaId: string,128  action: "add" | "remove",129  item: FavItem,130): Promise<boolean> {131  const ts = Math.floor(Date.now() / 1000);132  const s = sig(kaId, ts);133  if (!s || !linked(kaId) || (action !== "add" && action !== "remove")) return false;134  const r = await hubFetch(`${KA_HUB_URL}/api/sso/favorites`, {135    method: "POST",136    headers: { "Content-Type": "application/json" },137    body: JSON.stringify({138      client_id: CLIENT_ID,139      ka_id: kaId,140      ts: String(ts),141      sig: s,142      action,143      item,144    }),145  });146  const ok = r?.status === 200;147  if (ok) cache.delete(kaId); // la prochaine lecture reflète le toggle148  return ok;149}150