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%

kaid/ : kaid.ts — client KA ID v2 pour apps Next (track/fetchPrefs/affinity, HMAC serveur, fail-open)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 29 days ago (Aug 26, 2026) parent 9be7acc

1 changed file +124 −0

added kaid/kaid.ts +124 −0
@@ -0,0 +1,124 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// -----------------------------------------------------------------------------
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 des
5 +// apps TypeScript (vrai-prix, valoplex, trouve-ka). Ne pas diverger.
6 +//
7 +// Équivalent TS de kaid.py : track() (journal, fire-and-forget) et
8 +// fetchPrefs() (profil appris, cache 90 s, fail-open). Appels SERVEUR
9 +// 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 +// -----------------------------------------------------------------------------
13 +import { createHmac } from "node:crypto";
14 +
15 +export 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 +};
26 +
27 +export 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;
40 +
41 +const TIMEOUT_MS = 5_000;
42 +const PREFS_TTL_MS = 90_000;
43 +const prefsCache = new Map<string, { t: number; data: KaidPrefs }>();
44 +
45 +function hubUrl(): string {
46 + return (process.env.KA_HUB_URL ?? "https://www.groupe-ka.com").replace(/\/+$/, "");
47 +}
48 +
49 +function 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 +}
58 +
59 +/** Journalise des événements au hub — fire-and-forget, jamais bloquant. */
60 +export 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 +}
79 +
80 +/** Profil de personnalisation (cache 90 s) — null si indisponible (fail-open). */
81 +export 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 +}
104 +
105 +/** Affinité [−1,1] d'une valeur dans une dimension du profil (0 si inconnue). */
106 +export 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 +}
115 +
116 +/** Affinité de localisation transversale (tous univers confondus). */
117 +export 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