SPB Git forge

spb/valoplex

Public

ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.

11commits 1branches 0releases
2.4 MBsize
maindefault branch
20 days agolast push
TypeScript 91.9% Python 6% CSS 2.1%
4.2 KB · 130 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// -----------------------------------------------------------------------------3// hubfav — favoris « Mon univers Ka » : le hub Groupe KA (groupe-ka.com) est le4// MAGASIN CENTRAL des favoris du groupe — ValoPlex ne stocke RIEN localement.5// Chaque ♥ est poussé au hub (POST signé HMAC, synchrone : un échec remonte à6// l'appelant) et la liste est lue au hub (GET signé, cache mémoire 30 s,7// invalidé à chaque toggle). Même secret que le SSO (KA_SSO_SECRET).8//   sig = HMAC-SHA256(KA_SSO_SECRET, "valoplex.<ka_id>.<ts>") en hex,9//   ts en secondes epoch (fenêtre ±300 s côté hub).10// -----------------------------------------------------------------------------11import { createHmac } from "node:crypto";12import { CLIENT_ID, KA_HUB_URL } from "@/lib/ka-auth";1314const CACHE_TTL_MS = 30_000; // liste des favoris15const TIMEOUT_MS = 6_000;1617/** Item de favori tel qu'accepté par le hub (champ → longueur maximale). */18const FIELDS: Record<string, number> = {19  item_id: 120,20  title: 200,21  subtitle: 200,22  price_label: 60,23  image_url: 500,24  url: 500,25};2627export type FavItem = {28  item_id: string;29  title: string;30  subtitle?: string;31  price_label?: string;32  image_url?: string;33  url?: string;34} & Record<string, unknown>;3536const cache = new Map<string, { at: number; favs: FavItem[] }>();3738function sig(kaId: string, ts: number): string | null {39  const secret = process.env.KA_SSO_SECRET;40  if (!secret) return null;41  return createHmac("sha256", secret)42    .update(`${CLIENT_ID}.${kaId}.${ts}`)43    .digest("hex");44}4546/** Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe). */47export function linked(kaId: string | null | undefined): boolean {48  return typeof kaId === "string" && kaId.startsWith("ka-");49}5051/** Ne garde que les champs d'item connus, en chaînes tronquées. */52export function cleanItem(raw: Record<string, unknown>): FavItem {53  const out: Record<string, string> = {};54  for (const [k, max] of Object.entries(FIELDS)) {55    const v = raw?.[k];56    if (v != null && String(v)) out[k] = String(v).slice(0, max);57  }58  return out as FavItem;59}6061/** Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :62 *  le hub est le magasin des favoris, l'échec doit remonter à l'appelant. */63export async function hubToggle(64  kaId: string,65  action: string,66  item: FavItem,67): Promise<boolean> {68  const ts = Math.floor(Date.now() / 1000);69  const s = sig(kaId, ts);70  if (!s || !linked(kaId) || (action !== "add" && action !== "remove")) {71    return false;72  }73  let ok = false;74  try {75    const r = await fetch(`${KA_HUB_URL}/api/sso/favorites`, {76      method: "POST",77      headers: { "Content-Type": "application/json" },78      signal: AbortSignal.timeout(TIMEOUT_MS),79      body: JSON.stringify({80        client_id: CLIENT_ID,81        ka_id: kaId,82        ts: String(ts),83        sig: s,84        action,85        item,86      }),87    });88    ok = r.status === 200;89  } catch {90    ok = false;91  }92  if (ok) cache.delete(kaId); // la prochaine lecture reflète le toggle93  return ok;94}9596/** Favoris ValoPlex du membre, lus au hub (cache mémoire 30 s).97 *  [] = aucun favori ; null = hub injoignable (erreur, jamais mise en cache). */98export async function hubList(kaId: string): Promise<FavItem[] | null> {99  if (!linked(kaId)) return []; // compte non relié au hub100  const now = Date.now();101  const hit = cache.get(kaId);102  if (hit && now - hit.at < CACHE_TTL_MS) return hit.favs;103  const ts = Math.floor(now / 1000);104  const s = sig(kaId, ts);105  if (!s) return null;106  let favs: FavItem[];107  try {108    const q = new URLSearchParams({109      client_id: CLIENT_ID,110      ka_id: kaId,111      ts: String(ts),112      sig: s,113    });114    const r = await fetch(`${KA_HUB_URL}/api/sso/favorites?${q}`, {115      signal: AbortSignal.timeout(TIMEOUT_MS),116      cache: "no-store",117    });118    if (r.status !== 200) return null;119    const body = (await r.json()) as { favorites?: unknown };120    if (!Array.isArray(body.favorites)) return null;121    favs = body.favorites.filter(122      (f): f is FavItem => typeof f === "object" && f !== null,123    );124  } catch {125    return null;126  }127  cache.set(kaId, { at: now, favs });128  return favs;129}130