Python 33.5%
JavaScript 30.1%
TypeScript 25%
CSS 10%
Shell 1.4%
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