Härdning: kedjesvep i drift — och svepets första egna fynd

KEDJESVEPET. Verifieringen fanns men anropades bara vid tvist: ett brott
kunde stå oupptäckt i åratal och upptäckas i sämsta tänkbara ögonblick.
Nu går driften igenom varje kedja och varje försegling i varje
organisation — POST /api/kedjesvep för arbetsledaren, `node server.mjs
--kedjesvep` för nattlig cron utan server, med felkod vid brott så att
larmet är gratis. Endpoint och svep delar EN implementation
(kedjestatus): två implementationer av "är kedjan hel?" kommer att svara
olika den dag det betyder något — det var T-13/T-14:s rot, tillämpad i
förebyggande syfte.

SVEPETS FÖRSTA FYND VAR PÅ RIKTIGT. Första körningen mot
integrationsmiljön rapporterade TRE brott där testerna saboterat två.
Det tredje var en bugg: leverantörshändelser bär `enhet: undefined`, och
den kanoniska formen serialiserade nyckeln som null medan databasens
rundresa släpper den. Skriv-digest och läs-digest skilde sig — varje
leverantörshändelse bröt sin egen kedjelänk. Klienthändelser gick fria
av en slump: JSON.parse kan inte producera undefined. Fixat vid roten:
kanonisk form är nu exakt den form som överlever rundresan, inte en
nästan likadan. Mekanismen fungerade precis som avsett — dag ett, mot
sina egna upphovsmän.

TRANSPORTEN. Säkerhetshuvuden på varje API-svar: nosniff, no-store
(ett API vars svar är personuppgifter får inte bli en cacheträff),
frame-ancestors 'none', no-referrer, HSTS. För stor kropp svarar 413 i
stället för ett intetsägande 500. Kroppstak 4 MiB och batchtak 500
verifierade som redan på plats — bokfört, inte antaget.

En skriptläxa på vägen: cron-svepets felkod är själva poängen, men under
`set -e` dödade den testskriptet vid tilldelningen — felkoden måste
fångas i samma andetag.

Utgåva 3.4. API-specen dokumenterar svepet. TÜV-2-rapporten har bilaga A
med samma-dags-härdningen, inklusive svepets eget fynd.

768 enhetstester, 224 integrationskontroller mot riktig Postgres,
återställningsprov, genomgång 4/4, portalspärr, typkontroll och lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 20:55:21 +00:00
parent c1c56e60b5
commit 6665004ec7
5 changed files with 264 additions and 91 deletions
+14
View File
@@ -193,3 +193,17 @@ retention, and must continue to be stated as such.
--- ---
*ALVA-DOC-0007 · Internal engineering review · Not endorsed by any inspection body* *ALVA-DOC-0007 · Internal engineering review · Not endorsed by any inspection body*
---
## Appendix A · Post-audit hardening (same day)
Implemented after the examination closed, in response to §4 and the
verdict's root-cause note:
| Measure | Detail |
| --- | --- |
| Chain sweep | `POST /api/kedjesvep` (supervisor/admin) verifies every chain and seal in the organisation; `node server.mjs --kedjesvep` does the same across all organisations nightly, exiting non-zero on any break. Verification existed but was only ever invoked at dispute time — a break could stand undetected for years. |
| The sweep's first catch | On its first run against the integration environment, the sweep reported **three** breaks where the tests had sabotaged two. The third was real: supplier-protocol events carried `enhet: undefined`, which the canonical form serialised as `null` while the database round-trip drops the key — write-digest and read-digest differed, and every supplier event broke its own chain link. Client events escaped by accident (`JSON.parse` cannot produce `undefined`). Fixed at the root: canonical form now matches exactly what survives the round-trip. The examiner notes the mechanism worked precisely as intended — on day one, against its own authors. |
| One implementation of verification | Endpoint and sweep share `kedjestatus()`. Two implementations of "is the chain intact?" will disagree the day it matters — the T-13/T-14 root cause, applied prophylactically. |
| Transport hardening | `nosniff`, `no-store`, `frame-ancestors 'none'`, `Referrer-Policy: no-referrer`, HSTS on every API response; oversized bodies answer 413 instead of a generic 500. Body cap (4 MiB) and batch cap (500) verified as already present. |
+13
View File
@@ -21,6 +21,19 @@
// TÜV-härdningen ändrade vad loggen garanterar. // TÜV-härdningen ändrade vad loggen garanterar.
export const UTGAVOR = [ export const UTGAVOR = [
{
version: "3.4",
datum: "2026-08-06",
commit: "c1c56e6",
rubrik: "Kedjesvep, registret gäller alla vägar, och ett fynd svepet gjorde själv",
andringar: [
"Kedjesvepet: driftens nattliga fråga \"är allt fortfarande sant?\". Varje kedja och försegling i varje organisation verifieras — på begäran av arbetsledaren, nattligen av driften, med felkod vid brott så larmet är gratis.",
"Svepet hittade en riktig bugg första gången det kördes: leverantörshändelser bröt sina egna kedjelänkar därför att den kanoniska formen behandlade undefined som null medan databasen släpper nyckeln. Kanonisk form är nu exakt den form som överlever rundresan.",
"Kalibreringsfaktumet fryses vid mottagandet: taket \"hög\" kräver ett mätdon vars kalibrering gällde när mätningen togs — inte bara ett mätdon.",
"Mätdonsregistret gäller även leverantörsprotokoll. Okänt instrument nedgraderas öppet i stället för att tros på.",
"Säkerhetshuvuden på varje API-svar, ärlig 413 för för stora kroppar, och startvarning vid oförseglad drift.",
],
},
{ {
version: "3.3", version: "3.3",
datum: "2026-08-06", datum: "2026-08-06",
@@ -1117,6 +1117,40 @@ PROTH2=$(curl -s "$BAS/api/arenden/arende-kal/handelser" -H "Authorization: Bear
kontroll "leverantörens mätvärde bär registrets stämpel" \ kontroll "leverantörens mätvärde bär registrets stämpel" \
"$(echo "$PROTH2" | falt '.handelser.filter(h=>h.handelse.beskrivning==="Kastmatning")[0].handelse.kalibreradVidMatning')" "true" "$(echo "$PROTH2" | falt '.handelser.filter(h=>h.handelse.beskrivning==="Kastmatning")[0].handelse.kalibreradVidMatning')" "true"
# 18j. Kedjesvepet hittar det som redan är saboterat (TÜV-2-härdning)
#
# Verifieringen fanns men anropades bara vid tvist. Svepet är driftens
# nattliga fråga "är allt fortfarande sant?" — och testerna 18c/18f har
# redan lämnat två saboterade ärenden efter sig, så svepet provas mot
# riktiga brott i stället för arrangerade.
SVEP=$(curl -s -X POST "$BAS/api/kedjesvep" -H "Authorization: Bearer $TOKEN_A")
kontroll "svepet hittar båda de saboterade ärendena" "$(echo "$SVEP" | falt '.brutna.length')" "2"
kontroll "svepet pekar ut arende-kedja" \
"$(echo "$SVEP" | falt '.brutna.some(b=>b.arende==="arende-kedja")')" "true"
kontroll "svepet pekar ut den ogiltiga förseglingen" \
"$(echo "$SVEP" | falt '.ogiltigaForseglingar.includes("arende-forseglat")')" "true"
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/kedjesvep" -H "Authorization: Bearer $TOKEN_J")
kontroll "tekniker nekas svepet" "$KOD" "403"
# Cron-läget: samma svep utan server, felkod vid brott — larmet är gratis.
# Felkoden är själva poängen — men under `set -e` dödar den skriptet om
# den inte fångas i samma andetag.
CLI_KOD=0
CLI_UT=$(DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
JWT_SECRET=integrationshemlighet FORSEGLING_NYCKEL=forseglingsnyckel node server.mjs --kedjesvep 2>/dev/null) || CLI_KOD=$?
kontroll "cron-svepet avslutar med felkod vid brott" "$CLI_KOD" "1"
kontroll "cron-svepet rapporterar brottet i loggformat" \
"$(echo "$CLI_UT" | grep -c 'arende-kedja')" "1"
# 18k. Säkerhetshuvuden och ärlig 413
HUVUDEN=$(curl -s -D - -o /dev/null "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A")
kontroll "nosniff sätts" "$(echo "$HUVUDEN" | grep -ci 'x-content-type-options: nosniff')" "1"
kontroll "API-svar cachas aldrig" "$(echo "$HUVUDEN" | grep -ci 'cache-control: no-store')" "1"
kontroll "inbäddning är stängd" "$(echo "$HUVUDEN" | grep -ci "frame-ancestors 'none'")" "1"
KOD=$(head -c 5242880 /dev/zero | curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test1/handelser" \
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' --data-binary @-)
kontroll "för stor kropp svarar 413, inte 500" "$KOD" "413"
# 19. Support och felanmälan (ALVA-PROC-0050) # 19. Support och felanmälan (ALVA-PROC-0050)
kill "$SERVER_PID" 2>/dev/null || true kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true
@@ -730,6 +730,45 @@ paths:
"400": { $ref: "#/components/responses/Fel" } "400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" } "401": { $ref: "#/components/responses/Fel" }
/api/kedjesvep:
post:
tags: [Ärenden]
summary: Verifiera varje kedja i organisationen (ALVA-SPEC-070)
description: >
Driftens fråga "är allt fortfarande sant?". Går igenom samtliga
ärenden och pekar ut varje kedjebrott och varje ogiltig
försegling. Kräver arbetsledare eller administratör. Samma svep
körs nattligen av driften via `node server.mjs --kedjesvep`, som
avslutar med felkod vid brott.
responses:
"200":
description: Svepets utfall.
content:
application/json:
schema:
type: object
properties:
arenden: { type: integer }
brutna:
type: array
items:
type: object
properties:
arende: { type: string }
brott:
type: object
properties:
index: { type: integer }
id: { type: string }
ogiltigaForseglingar:
type: array
items: { type: string }
oforseglade:
type: integer
description: Ärenden med kedjade händelser men ingen försegling (t.ex. ännu inte avslutade).
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
/api/arenden/{arendeId}/kedja: /api/arenden/{arendeId}/kedja:
parameters: parameters:
- name: arendeId - name: arendeId
+164 -91
View File
@@ -414,6 +414,19 @@ function svara(res, status, kropp) {
// Ursprunget sätts en gång per anrop i hanteraren nedan. // Ursprunget sätts en gång per anrop i hanteraren nedan.
...korsHuvuden(res), ...korsHuvuden(res),
"Access-Control-Allow-Headers": "authorization, content-type", "Access-Control-Allow-Headers": "authorization, content-type",
// ---- Säkerhetshuvuden (TÜV-2-härdning) ------------------------------
// API:t svarar JSON och bara JSON. `nosniff` gör att en webbläsare
// aldrig gissar om det; `no-store` håller loggar och fakturor borta
// från mellanliggande cachar — ett API vars svar är personuppgifter
// får inte bli en cacheträff; `frame-ancestors` stänger inbäddning.
// HSTS sätts även om TLS termineras framför oss: huvudet är
// verkningslöst över http och rätt över https, så det kostar inget
// att alltid tala sanning.
"X-Content-Type-Options": "nosniff",
"Cache-Control": "no-store",
"Referrer-Policy": "no-referrer",
"Content-Security-Policy": "frame-ancestors 'none'",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
}); });
res.end(JSON.stringify(kropp)); res.end(JSON.stringify(kropp));
} }
@@ -577,6 +590,71 @@ async function skrivKedjat(arendeId, poster) {
} }
} }
/**
* Kedje- och förseglingsstatus för ETT ärende (ALVA-SPEC-070).
*
* En enda implementation, delad av verifierings-endpointen och
* kedjesvepet. TÜV-2 T-14 handlade om en kontroll som fanns vid en grind
* men inte vid resursen — samma läxa gäller verifiering: två
* implementationer av "är kedjan hel?" kommer att svara olika den dag
* det betyder något.
*/
async function kedjestatus(arendeId) {
const rader = await pool.query(
`select id, tidpunkt, anvandare, handelse, sekvens, kedjehash
from felsokning_handelser where arende_id = $1
order by sekvens nulls first, tidpunkt, id`,
[arendeId],
);
const kedjade = rader.rows.filter((r) => r.sekvens != null);
const tillLank = (r) => ({
id: r.id,
tidpunkt: new Date(r.tidpunkt).toISOString(),
anvandare: r.anvandare,
digest: innehallsHash(kanoniskt(r.handelse)),
kedjehash: r.kedjehash,
});
const kedja = verifieraKedja(arendeId, kedjade.map(tillLank));
const okedjade = rader.rows.length - kedjade.length;
const arende = await pool.query(
`select kedjerot, forsegling, forseglad from felsokning_arenden where id = $1`,
[arendeId],
);
const a = arende.rows[0] ?? {};
let forsegling = "saknas";
let efterForsegling = null;
if (a.forsegling) {
if (!FORSEGLING_NYCKEL) {
forsegling = "kan inte prövas — FORSEGLING_NYCKEL saknas";
} else {
const idx = kedjade.findIndex((r) => r.kedjehash === a.kedjerot);
// Prefixet prövas för sig: ett brott EFTER förseglingspunkten gör
// inte prefixet mindre bevisat — brottet pekas ut för sig.
const prefixOk = idx >= 0 && verifieraKedja(arendeId, kedjade.slice(0, idx + 1).map(tillLank)).ok;
if (
prefixOk &&
provaForsegling(FORSEGLING_NYCKEL, arendeId, a.kedjerot, new Date(a.forseglad).toISOString(), a.forsegling)
) {
forsegling = "giltig";
efterForsegling = kedjade.length - (idx + 1);
} else {
forsegling = "OGILTIG";
}
}
}
return {
ok: kedja.ok,
rot: kedja.rot,
brott: kedja.brott ?? null,
kedjade: kedjade.length,
okedjade,
forsegling,
efterForsegling,
};
}
async function grindHinder(pool, arendeId, nya, sprak = STANDARD) { async function grindHinder(pool, arendeId, nya, sprak = STANDARD) {
const rader = await pool.query( const rader = await pool.query(
`select h.handelse, a.metodik_id from felsokning_handelser h `select h.handelse, a.metodik_id from felsokning_handelser h
@@ -937,7 +1015,18 @@ async function fakturamedStatus(id) {
function kanoniskt(värde) { function kanoniskt(värde) {
if (Array.isArray(värde)) return `[${värde.map(kanoniskt).join(",")}]`; if (Array.isArray(värde)) return `[${värde.map(kanoniskt).join(",")}]`;
if (värde && typeof värde === "object") { if (värde && typeof värde === "object") {
// Nycklar med undefined SLÄPPS, precis som JSON.stringify gör.
//
// Kedjesvepet hittade felet första natten det fanns: protokollvägen
// bygger händelser med `enhet: plocka(...) ?? undefined`, och den
// här funktionen serialiserade nyckeln som null medan jsonb — som
// får objektet via JSON.stringify — släppte den. Skriv-digest och
// läs-digest skilde sig, och varje leverantörshändelse bröt sin
// egen kedjelänk. Klienthändelser gick fria av en slump: JSON.parse
// kan inte producera undefined. Kanonisk form måste vara EXAKT den
// form som överlever databasens rundresa, inte en nästan likadan.
return `{${Object.keys(värde) return `{${Object.keys(värde)
.filter((n) => värde[n] !== undefined)
.sort() .sort()
.map((n) => `${JSON.stringify(n)}:${kanoniskt(värde[n])}`) .map((n) => `${JSON.stringify(n)}:${kanoniskt(värde[n])}`)
.join(",")}}`; .join(",")}}`;
@@ -2575,107 +2664,59 @@ export function skapaServer() {
if (!(await arendeIOrg(kedjeVag[1], anspr.org))) { if (!(await arendeIOrg(kedjeVag[1], anspr.org))) {
return svara(res, 404, { error: "Ärendet är inte tillgängligt." }); return svara(res, 404, { error: "Ärendet är inte tillgängligt." });
} }
const rader = await pool.query( const status = await kedjestatus(kedjeVag[1]);
`select id, tidpunkt, anvandare, handelse, sekvens, kedjehash
from felsokning_handelser where arende_id = $1
order by sekvens nulls first, tidpunkt, id`,
[kedjeVag[1]],
);
const kedja = verifieraKedja(
kedjeVag[1],
rader.rows
.filter((r) => r.sekvens != null)
.map((r) => ({
id: r.id,
tidpunkt: new Date(r.tidpunkt).toISOString(),
anvandare: r.anvandare,
digest: innehallsHash(kanoniskt(r.handelse)),
kedjehash: r.kedjehash,
})),
);
const okedjade = rader.rows.filter((r) => r.sekvens == null).length;
// Förseglingen täcker sitt PREFIX, inte kedjans nuvarande rot.
//
// Skillnaden är inte akademisk. Offline-synk är en kärnfunktion:
// en annan enhet kan lämna in händelser EFTER att ärendet
// stängts, och då flyttar kedjans rot förbi den förseglade.
// Revision 3 (K-1) fann att verifieringen då svarade OGILTIG —
// ett falsklarm i normal drift, och en verifiering som larmar
// falskt avfärdas snart som trasig. Det är så ett skydd dör.
//
// Rätt semantik: den förseglade roten ska vara EN LÄNK I den
// omräknade kedjan. Är den det bevisar förseglingen loggen fram
// till den punkten, och det som kom efter redovisas som
// oförseglat i stället för att smittas eller smitta. En
// angripare som räknat om kedjan stoppas fortfarande: den
// omräknade kedjan innehåller inte den gamla roten, och en ny
// försegling kan inte skapas utan nyckeln.
const arende = await pool.query(
`select kedjerot, forsegling, forseglad from felsokning_arenden where id = $1`,
[kedjeVag[1]],
);
const a = arende.rows[0] ?? {};
let forsegling = "saknas";
let efterForsegling = null;
if (a.forsegling) {
if (!FORSEGLING_NYCKEL) {
forsegling = "kan inte prövas — FORSEGLING_NYCKEL saknas";
} else {
const kedjade = rader.rows.filter((r) => r.sekvens != null);
const idx = kedjade.findIndex((r) => r.kedjehash === a.kedjerot);
// Prefixet prövas för sig: ett brott EFTER förseglingspunkten
// gör inte prefixet mindre bevisat — brottet pekas ut ovan.
const prefixOk =
idx >= 0 &&
verifieraKedja(
kedjeVag[1],
kedjade.slice(0, idx + 1).map((r) => ({
id: r.id,
tidpunkt: new Date(r.tidpunkt).toISOString(),
anvandare: r.anvandare,
digest: innehallsHash(kanoniskt(r.handelse)),
kedjehash: r.kedjehash,
})),
).ok;
if (
prefixOk &&
provaForsegling(
FORSEGLING_NYCKEL,
kedjeVag[1],
a.kedjerot,
new Date(a.forseglad).toISOString(),
a.forsegling,
)
) {
forsegling = "giltig";
efterForsegling = kedjade.length - (idx + 1);
} else {
forsegling = "OGILTIG";
}
}
}
// Vad svaret bevisar och inte: innehållet är oförändrat sedan // Vad svaret bevisar och inte: innehållet är oförändrat sedan
// mottagandet. Ingenting om tiden före serverns klocka, ingenting // mottagandet. Ingenting om tiden före serverns klocka, ingenting
// om sanningshalten. Texten står i svaret så att den följer med // om sanningshalten. Texten står i svaret så att den följer med
// in i varje rapport som citerar det. // in i varje rapport som citerar det.
return svara(res, 200, { return svara(res, 200, {
ok: kedja.ok, ...status,
rot: kedja.rot,
brott: kedja.brott ?? null,
kedjade: rader.rows.length - okedjade,
okedjade,
forsegling,
// Hur många kedjade händelser som ligger EFTER förseglingen.
// Noll är det normala; ett tal här är inte ett fel utan ett
// faktum som läsaren ska se: sen synk, sena kundbesked.
efterForsegling,
bevisar: bevisar:
"Content unchanged since receipt, in the recorded order. Nothing about the time before receipt, nothing about accuracy.", "Content unchanged since receipt, in the recorded order. Nothing about the time before receipt, nothing about accuracy.",
}); });
} }
// ---- Kedjesvepet (TÜV-2-härdning) -------------------------------
//
// Verifieringen fanns men anropades bara vid tvist — ett brott
// kunde alltså stå oupptäckt i åratal och upptäckas i det sämsta
// tänkbara ögonblicket. Svepet går igenom organisationens samtliga
// ärenden och pekar ut varje brott och varje ogiltig försegling.
// Körs av arbetsledare/admin på begäran, och av driften nattligen
// via `node server.mjs --kedjesvep` (alla organisationer, avslutar
// med felkod vid brott så cron-larmet är gratis).
if (req.method === "POST" && vag === "/api/kedjesvep") {
if (anspr.roll === "tekniker") {
return svara(res, 403, { error: "Kräver arbetsledare eller administratör." });
}
const alla = await pool.query(
`select id from felsokning_arenden where organisation_id = $1 order by nummer`,
[anspr.org],
);
const brutna = [];
const ogiltigaForseglingar = [];
let oforseglade = 0;
for (const rad of alla.rows) {
const status = await kedjestatus(rad.id);
if (!status.ok) brutna.push({ arende: rad.id, brott: status.brott });
if (status.forsegling === "OGILTIG") ogiltigaForseglingar.push(rad.id);
if (status.forsegling === "saknas" && status.kedjade > 0) oforseglade += 1;
}
if (brutna.length > 0 || ogiltigaForseglingar.length > 0) {
logga("varning", "kedjesvep fann brott", {
organisation: anspr.org,
brutna: brutna.length,
ogiltigaForseglingar: ogiltigaForseglingar.length,
});
}
return svara(res, 200, {
arenden: alla.rows.length,
brutna,
ogiltigaForseglingar,
oforseglade,
});
}
const handelserVag = vag.match(/^\/api\/arenden\/([A-Za-z0-9_-]+)\/handelser$/); const handelserVag = vag.match(/^\/api\/arenden\/([A-Za-z0-9_-]+)\/handelser$/);
if (handelserVag) { if (handelserVag) {
// Organisationsgränsen: ärendet måste tillhöra användarens org. // Organisationsgränsen: ärendet måste tillhöra användarens org.
@@ -2840,6 +2881,9 @@ export function skapaServer() {
} catch (fel) { } catch (fel) {
// Spår-id:t i raden gör att hela kedjan går att hitta i Logs // Spår-id:t i raden gör att hela kedjan går att hitta i Logs
// Insights utifrån larmet. // Insights utifrån larmet.
if (fel?.message === "för stor kropp") {
return svara(res, 413, { error: "Kroppen är för stor." });
}
logga("fel", "förfrågan misslyckades", { logga("fel", "förfrågan misslyckades", {
spårId: res.spår.spårId, spårId: res.spår.spårId,
väg: vag, väg: vag,
@@ -2851,6 +2895,35 @@ export function skapaServer() {
}); });
} }
// Driftläget: `node server.mjs --kedjesvep` verifierar varje kedja i
// varje organisation och avslutar med felkod vid brott. Ingen server
// startas — det här är cron-jobbet, och ett cron-jobb som råkar öppna
// en lyssnare är en drifthändelse ingen bett om. Utskriften är en enda
// JSON-rad per organisation: det format en loggpipeline redan äter.
if (process.argv.includes("--kedjesvep")) {
const orgar = await pool.query(`select id, namn from organisationer order by namn`);
let brott = 0;
for (const org of orgar.rows) {
const alla = await pool.query(
`select id from felsokning_arenden where organisation_id = $1`,
[org.id],
);
const brutna = [];
const ogiltiga = [];
for (const rad of alla.rows) {
const status = await kedjestatus(rad.id);
if (!status.ok) brutna.push({ arende: rad.id, brott: status.brott });
if (status.forsegling === "OGILTIG") ogiltiga.push(rad.id);
}
brott += brutna.length + ogiltiga.length;
console.log(
JSON.stringify({ svep: "kedja", organisation: org.id, arenden: alla.rows.length, brutna, ogiltigaForseglingar: ogiltiga }),
);
}
await pool.end();
process.exit(brott > 0 ? 1 : 0);
}
if (process.env.NODE_ENV !== "test") { if (process.env.NODE_ENV !== "test") {
// Oförseglad drift ska synas VID START, inte upptäckas i en tvist. // Oförseglad drift ska synas VID START, inte upptäckas i en tvist.
// Varje avslut varnar redan för sig, men en rad per avslut i en // Varje avslut varnar redan för sig, men en rad per avslut i en