04e0a1e8ec
Guidad Felsökning / test-och-bygg (push) Has been cancelled
Guidad Felsökning / tjanster (push) Has been cancelled
Guidad Felsökning / terraform (push) Has been cancelled
Guidad Felsökning / leverantorskedja (push) Has been cancelled
Guidad Felsökning / aterstallning (push) Has been cancelled
Guidad Felsökning / publicera (push) Has been cancelled
Guidad Felsökning / driftsatt (push) Has been cancelled
189 lines
7.7 KiB
JavaScript
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 !== "");
|
|
}
|