SPB Git forge

spb/ka-ui

Public
30commits 1branches 0releases
145.7 MBsize
maindefault branch
27 days agolast push
Python 33.5% JavaScript 30.1% TypeScript 25% CSS 10% Shell 1.4%
4.1 KB · 125 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// -----------------------------------------------------------------------------3// Groupe KA — kaid.ts : client KA ID v2 (personnalisation) pour les apps Next.4// SOURCE CANONIQUE : ka-ui.git/kaid/kaid.ts — copié dans src/lib/kaid.ts des5// apps TypeScript (vrai-prix, valoplex, trouve-ka). Ne pas diverger.6//7// Équivalent TS de kaid.py : track() (journal, fire-and-forget) et8// fetchPrefs() (profil appris, cache 90 s, fail-open). Appels SERVEUR9// uniquement (route handlers) — le secret SSO ne va jamais au navigateur.10//   sig = HMAC-SHA256(KA_SSO_SECRET, `${clientId}.${kaId}.${ts}`) en hex.11// Aucune dépendance : node:crypto + fetch natif.12// -----------------------------------------------------------------------------13import { createHmac } from "node:crypto";1415export type KaidEvent = {16  type: string;17  entity_type?: string;18  entity_id?: string;19  query?: string;20  filters?: Record<string, unknown>;21  position?: number;22  features?: Record<string, unknown>;23  dwell_ms?: number;24  session_id?: string;25};2627export type KaidPrefs = {28  ok: boolean;29  personalization: boolean;30  profile: {31    app: {32      n: number;33      dims: Record<string, { values: Record<string, number>; conf: number }>;34      ranges: Record<string, { p25: number; p50: number; p75: number; n: number }>;35    } | null;36    global: { location: { values: Record<string, number>; conf: number } };37  } | null;38  hidden: string[];39} | null;4041const TIMEOUT_MS = 5_000;42const PREFS_TTL_MS = 90_000;43const prefsCache = new Map<string, { t: number; data: KaidPrefs }>();4445function hubUrl(): string {46  return (process.env.KA_HUB_URL ?? "https://www.groupe-ka.com").replace(/\/+$/, "");47}4849function signedParams(clientId: string, kaId: string): URLSearchParams | null {50  const secret = process.env.KA_SSO_SECRET;51  if (!secret || !kaId.startsWith("ka-")) return null;52  const ts = String(Math.floor(Date.now() / 1000));53  const sig = createHmac("sha256", secret)54    .update(`${clientId}.${kaId}.${ts}`)55    .digest("hex");56  return new URLSearchParams({ client_id: clientId, ka_id: kaId, ts, sig });57}5859/** Journalise des événements au hub — fire-and-forget, jamais bloquant. */60export function track(61  clientId: string,62  kaId: string | null | undefined,63  events: KaidEvent[],64): void {65  if (!kaId || !events.length) return;66  const p = signedParams(clientId, kaId);67  if (!p) return;68  const body = JSON.stringify({69    ...Object.fromEntries(p),70    events: events.slice(0, 20),71  });72  fetch(`${hubUrl()}/api/sso/events`, {73    method: "POST",74    headers: { "Content-Type": "application/json" },75    body,76    signal: AbortSignal.timeout(TIMEOUT_MS),77  }).catch(() => {});78}7980/** Profil de personnalisation (cache 90 s) — null si indisponible (fail-open). */81export async function fetchPrefs(82  clientId: string,83  kaId: string | null | undefined,84): Promise<KaidPrefs> {85  if (!kaId) return null;86  const hit = prefsCache.get(kaId);87  if (hit && Date.now() - hit.t < PREFS_TTL_MS) return hit.data;88  const p = signedParams(clientId, kaId);89  if (!p) return null;90  let data: KaidPrefs = null;91  try {92    const r = await fetch(`${hubUrl()}/api/sso/prefs?${p}`, {93      signal: AbortSignal.timeout(TIMEOUT_MS),94    });95    if (r.ok) data = (await r.json()) as KaidPrefs;96  } catch {97    data = null;98  }99  prefsCache.set(kaId, { t: Date.now(), data });100  if (prefsCache.size > 500)101    for (const k of [...prefsCache.keys()].slice(0, 100)) prefsCache.delete(k);102  return data;103}104105/** Affinité [−1,1] d'une valeur dans une dimension du profil (0 si inconnue). */106export function affinity(107  prefs: KaidPrefs,108  dim: string,109  value: string | null | undefined,110): number {111  const d = prefs?.profile?.app?.dims?.[dim];112  if (!d || !value) return 0;113  return d.values[value.trim().toLowerCase()] ?? 0;114}115116/** Affinité de localisation transversale (tous univers confondus). */117export function globalLocationAffinity(118  prefs: KaidPrefs,119  value: string | null | undefined,120): number {121  const loc = prefs?.profile?.global?.location;122  if (!loc || !value) return 0;123  return (loc.values[value.trim().toLowerCase()] ?? 0) * (loc.conf || 0.3);124}125