ALVA deployment config
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
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
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
// 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 !== "");
|
||||
}
|
||||
Reference in New Issue
Block a user