Files
Claude 31727a4ea0 Härda enligt revision 2: stäng schemat, ta bort dubbelregeln
C-5. granskaHändelse itererade schemats nycklar och aldrig händelsens,
så okända fält accepterades och sparades ordagrant. Två garantier vilade
på motsatsen: krypto-shreddingen skyddar en fast lista av fältnamn, så
personuppgifter på en vanlig observation krypterades aldrig och överlevde
raderingen — och delningsfiltret är typnivå, så samma fält gick ut i
kundens delningslänk. Schemat är nu stängt, med varje valfritt fält
deklarerat. Det gäller även `kalla`: protokollinläsningen fungerade bara
därför att schemat var öppet. Avslaget är hårt, inte en tyst strykning.
Verifierat mot riktig trafik — samtliga händelser som klienten faktiskt
producerar passerar.

M-7. Klienten upprepar inte längre grindens regel utan anropar grinda()
och visar dess egna hinder. Villkoret hade glidit isär två gånger utan
att något test märkte det.

Det avslöjade omedelbart ett verkligt fel i grinden: den krävde
textresultat även på kontroller vars krav är foto — trots att
gränssnittet märker fältet "Observation (valfritt)". Servern hade alltså
nekat avslut på nästan varje riktigt ärende, och det syntes inte så länge
klienten hade ett eget och mildare villkor. Evidens graderas nu efter
kontrollens eget krav.

M-8. Protokollinläsningen svarar med utfall per händelse i stället för en
siffra, 207 vid delvis lyckad inläsning, innehållshärledda id:n i stället
för klockan, och en transaktion runt hela importen.

M-9. En genererad webhookhemlighet lämnas ut en gång vid skapandet.

m-8. Profilens vägslagning begränsas till egna egenskaper.
m-10. Ett mätvärde som kommer ur en kontroll redovisas en gång.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-05 18:19:11 +00:00

189 lines
7.7 KiB
JavaScript

// ALVA-SPEC-020 · Integration interface.
//
// Verkstaden har redan system: ett DMS, ett diagnosinstrument som
// producerar protokoll, ett videooffertsystem. ALVA ska koppla in sig i
// dem — inte ersätta dem, och inte kräva att de anpassar sig.
//
// ---- Vad jag INTE bygger ---------------------------------------------
//
// Färdiga klienter mot namngivna leverantörer. Jag känner inte Beonodes
// eller ServiceCams faktiska API:er, och en uppfunnen endpoint som ser
// färdig ut är sämre än en tom: den ser ut att fungera tills någon
// försöker, och då är felet dyrare att hitta.
//
// I stället: ett stabilt gränssnitt med PROFILER. En profil beskriver
// vad en kategori av system förväntar sig — fältnamn, format, riktning —
// och valideras mot leverantören innan den märks som `validated`. Tills
// dess står den som `draft`, och det syns i gränssnittet.
//
// Det är samma princip som Knowledge Sources: leverantören är data.
//
// ---- Riktningar --------------------------------------------------------
//
// IN Diagnosprotokoll, arbetsorder, fordonsdata. Blir händelser i
// loggen med källa och tidsstämpel bevarade.
// UT Ärendet, rapporten, slutsatsen, mediet. Levereras som webhook
// eller hämtas via API.
//
// Riktningen är inte symmetrisk. Inkommande data blir evidens och måste
// därför bära sin härkomst hela vägen; utgående data är en projektion
// och kan formas fritt.
/** Kategorier av system. Namnen är generiska med avsikt. */
export const KATEGORIER = {
diagnosprotokoll: {
namn: "Diagnostic protocol",
riktning: "in",
beskrivning: "Fault codes, live data and readouts from a diagnostic instrument.",
// Vad ALVA gör av det: varje avläsning blir evidens, inte en bilaga.
blir: ["matvarde", "observation", "foto"],
},
dms: {
namn: "Dealer management system",
riktning: "bada",
beskrivning: "Work orders in, completed cases and time records out.",
blir: ["arbetsorder_skannad", "objekt_identifierat"],
},
videooffert: {
namn: "Video quotation",
riktning: "bada",
beskrivning: "Video evidence in, the case record and closing statement out.",
blir: ["video", "foto"],
},
fordonsdata: {
namn: "Vehicle data",
riktning: "in",
beskrivning: "Identification, equipment level, service campaigns.",
blir: ["objekt_identifierat"],
},
garanti: {
namn: "Warranty administration",
riktning: "ut",
beskrivning: "Claim substantiation: evidence chain, closing statement, traceability package.",
blir: [],
},
forsakring: {
namn: "Insurance assessment",
riktning: "ut",
beskrivning: "Assessor-facing record: what was checked, what was ruled out, and why.",
blir: [],
},
skadekalkyl: {
namn: "Damage calculation",
riktning: "bada",
beskrivning:
"Damage estimation systems used between workshops and insurers — CABAS is the Nordic standard. " +
"In: damage assessment and vehicle identification. Out: the diagnostic record substantiating that " +
"the damage was investigated rather than assumed.",
blir: ["objekt_identifierat", "observation", "foto"],
// Det ALVA tillför en skadekalkyl är inte fler poster utan
// beviskedjan bakom dem: vad som kontrollerades, vad som uteslöts
// och varför. Det är den enda del av kalkylen som i dag inte går
// att granska i efterhand.
tillfor: ["slutsats", "felorsak", "reproducering"],
},
};
/**
* Profilens mognad. Samma resonemang som konnektorernas livscykel:
* "det finns kod" är inte samma sak som "det fungerar mot leverantören".
*/
export const MOGNAD = ["draft", "tested", "validated"];
/**
* Utgående händelser. En integration prenumererar på det den behöver —
* ett videooffertsystem bryr sig inte om varje mätvärde.
*/
export const UTGAENDE = {
"arende.skapat": "A case was opened.",
"arende.fas": "The case entered a new ALVA phase.",
"arende.slutsats": "A closing statement was recorded.",
"arende.avslutat": "The case was closed and passed the quality gate.",
"media.tillagt": "Photo or video evidence was added.",
"atgardsforslag.lamnat": "A repair proposal was issued to the customer.",
"kundbeslut.registrerat": "The customer's decision was recorded.",
};
/**
* Signerar en utgående leverans.
*
* HMAC över tidsstämpel och kropp, i det format nästan alla
* webhook-mottagare redan förstår. Tidsstämpeln ingår i signaturen så
* att en avlyssnad leverans inte går att spela upp igen.
*/
export function signeraLeverans(kropp, hemlighet, tidsstampel, hmac) {
const underlag = `${tidsstampel}.${kropp}`;
return `t=${tidsstampel},v1=${hmac("sha256", hemlighet).update(underlag).digest("hex")}`;
}
/**
* Verifierar en inkommande leverans från oss.
*
* Exponerad så att mottagaren kan använda exakt samma kod som avsändaren
* — de flesta integrationsfel uppstår i glappet mellan två
* implementationer av samma signatur.
*/
export function verifieraLeverans(kropp, huvud, hemlighet, hmac, timingSafeEqual, nu = Date.now()) {
const delar = Object.fromEntries(
String(huvud ?? "")
.split(",")
.map((d) => d.split("=")),
);
if (!delar.t || !delar.v1) return { ok: false, orsak: "Malformed signature header." };
// Fem minuter. Nog för klockglapp, kort nog att en fångad leverans
// inte går att spela upp i morgon.
if (Math.abs(nu - Number(delar.t) * 1000) > 300_000) return { ok: false, orsak: "Timestamp outside tolerance." };
const väntad = hmac("sha256", hemlighet).update(`${delar.t}.${kropp}`).digest("hex");
const a = Buffer.from(delar.v1);
const b = Buffer.from(väntad);
if (a.length !== b.length || !timingSafeEqual(a, b)) return { ok: false, orsak: "Signature mismatch." };
return { ok: true };
}
/**
* Normaliserar ett inkommande diagnosprotokoll till händelser.
*
* Profilen beskriver var värdena ligger i leverantörens format; den här
* funktionen gör dem till evidens. Härkomsten bevaras i varje händelse —
* ett värde som kommit utifrån får aldrig se ut som något teknikern
* själv mätt.
*/
export function protokollTillHandelser(protokoll, profil, kalla) {
const ut = [];
// Vägen kommer från leverantörsprofilen, alltså utifrån. Uppslaget
// begränsas till egna, uppräkningsbara egenskaper: annars når en
// profil med `__proto__` eller `constructor.prototype` fram till
// prototypkedjan. Läsningen är visserligen ofarlig i sig, men en
// extern indata ska inte ha den friheten (QUALITY-AUDIT-2 · m-8).
const plocka = (objekt, vag) =>
String(vag ?? "")
.split(".")
.reduce((o, n) => (o !== null && typeof o === "object" && Object.hasOwn(o, n) ? o[n] : undefined), objekt);
for (const kod of plocka(protokoll, profil?.felkoder?.vag) ?? []) {
ut.push({
typ: "observation",
text: `${plocka(kod, profil.felkoder.kod) ?? "?"}${plocka(kod, profil.felkoder.text) ?? "utan beskrivning"}`,
kalla,
});
}
for (const värde of plocka(protokoll, profil?.matvarden?.vag) ?? []) {
ut.push({
typ: "matvarde",
beskrivning: String(plocka(värde, profil.matvarden.beskrivning) ?? "Avläst värde"),
varde: String(plocka(värde, profil.matvarden.varde) ?? ""),
enhet: plocka(värde, profil.matvarden.enhet) ?? undefined,
// Instrumentets identitet följer med när leverantören lämnar den.
// Utan den nedgraderas värdet till E1 — ett avläst tal utan känt
// instrument är inte en spårbar mätning (QUALITY-AUDIT M-1).
matdonId: plocka(värde, profil.matvarden.instrumentId) ?? undefined,
matdonBeteckning: plocka(värde, profil.matvarden.instrument) ?? undefined,
kalla,
});
}
return ut.filter((h) => h.typ !== "matvarde" || h.varde !== "");
}