{"uuid": "ce99d405-8ad0-470b-93d8-cfebfef8c0e3", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-m99w-x7hq-7vfj", "type": "seen", "source": "https://gist.github.com/klemmanton-lgtm/8b54bdd2fe9905dc89b9c8ccf42f5fc4", "content": "diff --git a/.github/dependabot.yml b/.github/dependabot.yml\nindex 7ab7ccd..f26fcbc 100644\n--- a/.github/dependabot.yml\n+++ b/.github/dependabot.yml\n@@ -13,6 +13,15 @@ updates:\n       production-minor-patch:\n         dependency-type: \"production\"\n         update-types: [\"minor\", \"patch\"]\n+        # next ist AUSGENOMMEN und bekommt weiterhin einen eigenen PR.\n+        # Grund: Der Sprung auf next 16.3.0 ist blockiert, solange\n+        # @netlify/plugin-nextjs &lt;= 5.15.13 ist. Steckt next in der Gruppe,\n+        # h\u00e4ngt die ganze Gruppe daran fest \u2014 dreimal passiert (#319, #323,\n+        # #326), zuletzt mit einem v\u00f6llig harmlosen lucide-react-Bump als\n+        # Geisel. Ohne next kommen die \u00fcbrigen Minor/Patches wieder durch,\n+        # und next wird einzeln entschieden, wie es ohnehin geh\u00f6rt.\n+        exclude-patterns:\n+          - \"next\"\n       dev-dependencies:\n         dependency-type: \"development\"\n     ignore:\ndiff --git a/.github/workflows/security.yml b/.github/workflows/security.yml\nindex e139639..d7b6e46 100644\n--- a/.github/workflows/security.yml\n+++ b/.github/workflows/security.yml\n@@ -2,9 +2,9 @@ name: Security\n \n # Kostenloses Sicherheits-Grundnetz f\u00fcr das private pledform-Repo:\n #   - gitleaks  -&gt; Secret-Scan \u00fcber die gesamte Git-Historie (blockierend)\n-#   - npm audit -&gt; Dependency-Schwachstellen (Phase 1: NICHT-blockierend, s.u.)\n+#   - audit-ci -&gt; Dependency-Schwachstellen (BLOCKIEREND, Allowlist in audit-ci.jsonc)\n # Native GitHub-Secret-Scanning/Push-Protection ist f\u00fcr private Repos\n-# kostenpflichtig -&gt; wir nutzen gitleaks + npm audit + Dependabot stattdessen.\n+# kostenpflichtig -&gt; wir nutzen gitleaks + audit-ci + Dependabot stattdessen.\n \n on:\n   pull_request:\n@@ -55,17 +55,21 @@ jobs:\n         # --exit-code 1 -&gt; Fund = roter Check (ist Default, hier explizit dokumentiert)\n         run: gitleaks git --redact=100 --no-banner --exit-code 1\n \n-  npm-audit:\n-    name: Dependency-Audit (npm)\n+  dependency-audit:\n+    name: Dependency-Audit (audit-ci)\n     runs-on: ubuntu-latest\n-    # PHASE 1 \u2014 NICHT-BLOCKIEREND.\n-    # Grund: Baseline ist nicht clean. xlsx/SheetJS (ehemals 2x high ohne Fix) ist\n-    # entfernt; es bleiben aber weitere Findings ohne verf\u00fcgbaren Fix (z.B.\n-    # esbuild via @react-email/ui, high). Ein blockierender Check w\u00fcrde jede PR\n-    # ohne Auswegm\u00f6glichkeit stoppen.\n-    # PHASE 2 (sp\u00e4ter, wenn Baseline clean): n\u00e4chste Zeile entfernen -&gt; blockierend,\n-    # dann in den Branch-Protection-Rules als Required Check eintragen.\n-    continue-on-error: true\n+    # PHASE 2 \u2014 BLOCKIEREND (der fr\u00fchere `continue-on-error: true` ist weg).\n+    # Die Baseline ist jetzt clean: audit-ci l\u00e4sst genau die sechs Advisories\n+    # durch, die in audit-ci.jsonc GHSA-genau, begr\u00fcndet und mit Verfallsdatum\n+    # gelistet sind. Alles andere macht den Check rot.\n+    # Der Vorg\u00e4nger `npm audit --audit-level=high` lief mit continue-on-error und\n+    # meldete dauerhaft \u201efail\", ohne je etwas zu verhindern \u2014 ein Gate, das man\n+    # nicht mehr liest, ist schlechter als keines.\n+    # KEINE technische Merge-Sperre: Branch-Protection-Rules sind f\u00fcr private\n+    # Repos auf dem aktuellen GitHub-Plan nicht verf\u00fcgbar (API antwortet mit\n+    # 403 \u201eUpgrade to GitHub Pro\"). \u201eBlockierend\" hei\u00dft hier: der Check wird rot\n+    # und ist im PR sichtbar \u2014 durchgesetzt wird er durch Disziplin, nicht durch\n+    # GitHub. Sobald der Plan es hergibt: als Required Check eintragen.\n     steps:\n       - name: Checkout\n         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n@@ -82,5 +86,8 @@ jobs:\n       - name: Dependencies installieren (reproduzierbar)\n         run: npm ci\n \n-      - name: npm audit (high und h\u00f6her)\n-        run: npm audit --audit-level=high\n+      - name: audit-ci (high und h\u00f6her, Allowlist in audit-ci.jsonc)\n+        # Die Allowlist selbst wird von src/lib/utils/audit-allowlist.test.ts\n+        # bewacht: GHSA-genaue Eintr\u00e4ge, keine paketweiten, kein abgelaufenes\n+        # Verfallsdatum. Der Unit-Test-Workflow schl\u00e4gt sonst fehl.\n+        run: npx audit-ci --config ./audit-ci.jsonc\ndiff --git a/AGENTS.md b/AGENTS.md\nindex 7088936..b383353 100644\n--- a/AGENTS.md\n+++ b/AGENTS.md\n@@ -67,6 +67,7 @@ Diese Regeln stammen aus realen Bugs. Nicht davon abweichen, ohne R\u00fccksprache.\n **pgTAP / Testbelege:**\n - **`ERROR` ohne `not ok` ist trotzdem rot.** Ein Transaktionsabbruch erzeugt keine `not ok`-Zeile, sondern `ERROR: current transaction is aborted`. Wer nur `not ok` z\u00e4hlt, meldet Gr\u00fcn auf einer roten Suite. Immer zus\u00e4tzlich `^ERROR` z\u00e4hlen. Ebenso: `Result: NOTESTS` bei Exit 0 gilt als Fehlschlag, nicht als Erfolg. Ein pgTAP-Beleg ohne konkrete **Datei- UND Testzahl** ist kein Beleg.\n - **pgTAP VOR E2E fahren oder dazwischen `supabase db reset`.** Ein E2E-Lauf hinterl\u00e4sst echte Zeilen im lokalen Stack; danach brechen pgTAP-Annahmen, die auf einer sauberen Basis beruhen (z. B. \u201egenau zwei Fan-out-Zeilen\" in `256`, oder `084` mit \u201emore than one row returned by a subquery\"). Das sieht wie ein Regress aus und ist keiner.\n+- **Auch ein DB-Absturz tarnt sich als Gr\u00fcn.** Unter Colima segfaultet der lokale Postgres reproduzierbar in `097`, `241` und `244` (jeweils mitten in der Datei, \u201eserver terminated abnormally\"). Die Folgedateien scheitern dann an `psql: error: \u2026 the database system is in recovery mode` \u2014 was WEDER `not ok` NOCH eine mit `ERROR` beginnende Zeile erzeugt. Deshalb ist die belastbare Pr\u00fcfung nicht das Z\u00e4hlen von Treffern, sondern **`1..N` gegen die Zahl der `ok`/`not ok`-Zeilen je Datei**. Diese drei Dateien laufen lokal nicht durch; f\u00fcr sie ist allein CI ma\u00dfgeblich.\n - CI (`supabase test db` = pg_prove) wertet beides korrekt. Blind ist nur der lokale `docker exec`-Umweg, der n\u00f6tig ist, weil Colima `/private/tmp` nicht mountet (betrifft Worktrees au\u00dferhalb von `$HOME`).\n \n **Next.js 16 / React:**\ndiff --git a/audit-ci.jsonc b/audit-ci.jsonc\nnew file mode 100644\nindex 0000000..faebcd3\n--- /dev/null\n+++ b/audit-ci.jsonc\n@@ -0,0 +1,55 @@\n+// audit-ci \u2014 Dependency-Gate f\u00fcr pledform\n+// =============================================================================\n+// Ersetzt den fr\u00fcheren `npm audit --audit-level=high`-Schritt. Zwei Gates mit\n+// unterschiedlicher Meinung sind schlimmer als eines: der alte Schritt lief mit\n+// `continue-on-error: true` und meldete dauerhaft \u201efail\", ohne je etwas zu\n+// verhindern \u2014 CI-Ger\u00e4usch, das man irgendwann nicht mehr liest.\n+//\n+// Mit der Allowlist unten ist die Baseline sauber, deshalb ist das Gate jetzt\n+// BLOCKIEREND (das ist die im Workflow dokumentierte \u201ePhase 2\").\n+//\n+// ALLOWLIST-DISZIPLIN: Eintr\u00e4ge sind GHSA-genau, nie paketweit. `\"next\"` als\n+// Eintrag w\u00fcrde auch jedes ZUK\u00dcNFTIGE next-Advisory verschlucken \u2014 genau das,\n+// wogegen das Gate sch\u00fctzen soll.\n+//\n+// Jeder Eintrag tr\u00e4gt Begr\u00fcndung und Verfallsdatum. Das Datum ist nicht nur\n+// Kommentar: `src/lib/utils/audit-allowlist.test.ts` liest diese Datei und\n+// schl\u00e4gt fehl, sobald ein Eintrag abgelaufen ist. Eine Allowlist, die\n+// niemand mehr \u00fcberpr\u00fcft, ist eine stillgelegte Sicherung.\n+{\n+  // Schwelle wie beim Vorg\u00e4nger: high und h\u00f6her. Moderate Findings (uuid\n+  // GHSA-w5hq-g745-h8pq, diverse next/postcss) sind bewusst nicht erfasst.\n+  \"high\": true,\n+  \"critical\": true,\n+\n+  // \u2500\u2500 Allowlist \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n+  // ALLE sechs Eintr\u00e4ge h\u00e4ngen an DERSELBEN Ursache: dem blockierten Sprung auf\n+  // next 16.3.0. Sie fallen gemeinsam weg, sobald er m\u00f6glich ist.\n+  //\n+  // Gemeinsamer Ausl\u00f6ser f\u00fcr die Wiedervorlage:\n+  //   @netlify/plugin-nextjs &gt; 5.15.13  (bis dahin blockt die Netlify-Runtime\n+  //   den Sprung; deshalb liegen die Dependabot-PRs #319, #323, #326)\n+  // Gemeinsames Verfallsdatum: 2026-11-30\n+  \"allowlist\": [\n+    // \u2500\u2500 next: vier Treffer, die uns NICHT betreffen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n+    // npm kollabiert Stable- und 16.3-Preview-Range zu einem Intervall\n+    // (\"9.3.4-canary.0 - 16.3.0-preview.10\"). Das Juli-B\u00fcndel ist in 16.2.11\n+    // gepatcht, wir stehen auf 16.2.12. Belegt am 09.08.2026 gegen die\n+    // Vercel-Meldung; siehe AGENTS.md, Abschnitt \u201enpm audit / Dependency-Gate\".\n+    // Verf\u00e4llt: 2026-11-30\n+    \"GHSA-6gpp-xcg3-4w24\", // Middleware/Proxy bypass im App Router\n+    \"GHSA-89xv-2m56-2m9x\", // SSRF in Server Actions\n+    \"GHSA-m99w-x7hq-7vfj\", // DoS im App Router \u00fcber Server Actions\n+    \"GHSA-p9j2-gv94-2wf4\", // SSRF in rewrites\n+\n+    // \u2500\u2500 postcss: real, aber nur \u00fcber next erreichbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n+    // Beide Pfade sind `next&gt;postcss` bzw. `@react-email/ui&gt;next&gt;postcss` \u2014\n+    // kein direkter Import. postcss l\u00e4uft ausschlie\u00dflich zur BUILD-Zeit \u00fcber\n+    // unser eigenes CSS, verarbeitet also keine Angreifer-Eingaben. Ein Fix\n+    // kommt mit dem next-Upgrade, ein eigener Override w\u00e4re ein zweiter\n+    // Sonderweg neben dem sharp-Override.\n+    // Verf\u00e4llt: 2026-11-30\n+    \"GHSA-6g55-p6wh-862q\", // Arbitrary file read \u00fcber Source-Map-Aufl\u00f6sung\n+    \"GHSA-r28c-9q8g-f849\"  // Path Traversal beim Auto-Loading alter Source Maps\n+  ]\n+}\ndiff --git a/package-lock.json b/package-lock.json\nindex 56c7f78..9dc07d1 100644\n--- a/package-lock.json\n+++ b/package-lock.json\n@@ -44,6 +44,7 @@\n         \"@types/node\": \"^26\",\n         \"@types/react\": \"^19\",\n         \"@types/react-dom\": \"^19\",\n+        \"audit-ci\": \"^7.1.0\",\n         \"eslint\": \"^9\",\n         \"eslint-config-next\": \"^16.3.0\",\n         \"exceljs\": \"^4.4.0\",\n@@ -7464,6 +7465,16 @@\n         \"ajv\": \"^6.9.1\"\n       }\n     },\n+    \"node_modules/ansi-regex\": {\n+      \"version\": \"5.0.1\",\n+      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n+      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"engines\": {\n+        \"node\": \"&gt;=8\"\n+      }\n+    },\n     \"node_modules/ansi-styles\": {\n       \"version\": \"4.3.0\",\n       \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz\",\n@@ -7826,6 +7837,43 @@\n         \"when-exit\": \"^2.1.4\"\n       }\n     },\n+    \"node_modules/audit-ci\": {\n+      \"version\": \"7.1.0\",\n+      \"resolved\": \"https://registry.npmjs.org/audit-ci/-/audit-ci-7.1.0.tgz\",\n+      \"integrity\": \"sha512-PjjEejlST57S/aDbeWLic0glJ8CNl/ekY3kfGFPMrPkmuaYaDKcMH0F9x9yS9Vp6URhuefSCubl/G0Y2r6oP0g==\",\n+      \"dev\": true,\n+      \"license\": \"Apache-2.0\",\n+      \"dependencies\": {\n+        \"cross-spawn\": \"^7.0.3\",\n+        \"escape-string-regexp\": \"^4.0.0\",\n+        \"event-stream\": \"4.0.1\",\n+        \"jju\": \"^1.4.0\",\n+        \"jsonstream-next\": \"^3.0.0\",\n+        \"readline-transform\": \"1.0.0\",\n+        \"semver\": \"^7.0.0\",\n+        \"tslib\": \"^2.0.0\",\n+        \"yargs\": \"^17.0.0\"\n+      },\n+      \"bin\": {\n+        \"audit-ci\": \"dist/bin.js\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=16\"\n+      }\n+    },\n+    \"node_modules/audit-ci/node_modules/semver\": {\n+      \"version\": \"7.8.5\",\n+      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-7.8.5.tgz\",\n+      \"integrity\": \"sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==\",\n+      \"dev\": true,\n+      \"license\": \"ISC\",\n+      \"bin\": {\n+        \"semver\": \"bin/semver.js\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=10\"\n+      }\n+    },\n     \"node_modules/available-typed-arrays\": {\n       \"version\": \"1.0.7\",\n       \"resolved\": \"https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz\",\n@@ -8339,6 +8387,21 @@\n       \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\",\n       \"license\": \"MIT\"\n     },\n+    \"node_modules/cliui\": {\n+      \"version\": \"8.0.1\",\n+      \"resolved\": \"https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz\",\n+      \"integrity\": \"sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==\",\n+      \"dev\": true,\n+      \"license\": \"ISC\",\n+      \"dependencies\": {\n+        \"string-width\": \"^4.2.0\",\n+        \"strip-ansi\": \"^6.0.1\",\n+        \"wrap-ansi\": \"^7.0.0\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=12\"\n+      }\n+    },\n     \"node_modules/clsx\": {\n       \"version\": \"2.1.1\",\n       \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz\",\n@@ -8912,6 +8975,13 @@\n         \"node\": \"&gt;= 0.4\"\n       }\n     },\n+    \"node_modules/duplexer\": {\n+      \"version\": \"0.1.2\",\n+      \"resolved\": \"https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz\",\n+      \"integrity\": \"sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\"\n+    },\n     \"node_modules/duplexer2\": {\n       \"version\": \"0.1.4\",\n       \"resolved\": \"https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz\",\n@@ -9748,6 +9818,22 @@\n         \"node\": \"&gt;=0.10.0\"\n       }\n     },\n+    \"node_modules/event-stream\": {\n+      \"version\": \"4.0.1\",\n+      \"resolved\": \"https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz\",\n+      \"integrity\": \"sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"duplexer\": \"^0.1.1\",\n+        \"from\": \"^0.1.7\",\n+        \"map-stream\": \"0.0.7\",\n+        \"pause-stream\": \"^0.0.11\",\n+        \"split\": \"^1.0.1\",\n+        \"stream-combiner\": \"^0.2.2\",\n+        \"through\": \"^2.3.8\"\n+      }\n+    },\n     \"node_modules/events\": {\n       \"version\": \"3.3.0\",\n       \"resolved\": \"https://registry.npmjs.org/events/-/events-3.3.0.tgz\",\n@@ -9991,6 +10077,13 @@\n         \"url\": \"https://github.com/sponsors/ljharb\"\n       }\n     },\n+    \"node_modules/from\": {\n+      \"version\": \"0.1.7\",\n+      \"resolved\": \"https://registry.npmjs.org/from/-/from-0.1.7.tgz\",\n+      \"integrity\": \"sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\"\n+    },\n     \"node_modules/fs-constants\": {\n       \"version\": \"1.0.0\",\n       \"resolved\": \"https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz\",\n@@ -10096,6 +10189,16 @@\n         \"node\": \"&gt;=6.9.0\"\n       }\n     },\n+    \"node_modules/get-caller-file\": {\n+      \"version\": \"2.0.5\",\n+      \"resolved\": \"https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz\",\n+      \"integrity\": \"sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==\",\n+      \"dev\": true,\n+      \"license\": \"ISC\",\n+      \"engines\": {\n+        \"node\": \"6.* || 8.* || &gt;= 10.*\"\n+      }\n+    },\n     \"node_modules/get-intrinsic\": {\n       \"version\": \"1.3.0\",\n       \"resolved\": \"https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz\",\n@@ -10772,6 +10875,16 @@\n         \"url\": \"https://github.com/sponsors/ljharb\"\n       }\n     },\n+    \"node_modules/is-fullwidth-code-point\": {\n+      \"version\": \"3.0.0\",\n+      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n+      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"engines\": {\n+        \"node\": \"&gt;=8\"\n+      }\n+    },\n     \"node_modules/is-generator-function\": {\n       \"version\": \"1.1.2\",\n       \"resolved\": \"https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz\",\n@@ -11097,6 +11210,13 @@\n         \"jiti\": \"lib/jiti-cli.mjs\"\n       }\n     },\n+    \"node_modules/jju\": {\n+      \"version\": \"1.4.0\",\n+      \"resolved\": \"https://registry.npmjs.org/jju/-/jju-1.4.0.tgz\",\n+      \"integrity\": \"sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\"\n+    },\n     \"node_modules/js-tokens\": {\n       \"version\": \"4.0.0\",\n       \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n@@ -11191,6 +11311,33 @@\n         \"node\": \"&gt;=6\"\n       }\n     },\n+    \"node_modules/jsonparse\": {\n+      \"version\": \"1.3.1\",\n+      \"resolved\": \"https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz\",\n+      \"integrity\": \"sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==\",\n+      \"dev\": true,\n+      \"engines\": [\n+        \"node &gt;= 0.2.0\"\n+      ],\n+      \"license\": \"MIT\"\n+    },\n+    \"node_modules/jsonstream-next\": {\n+      \"version\": \"3.0.0\",\n+      \"resolved\": \"https://registry.npmjs.org/jsonstream-next/-/jsonstream-next-3.0.0.tgz\",\n+      \"integrity\": \"sha512-aAi6oPhdt7BKyQn1SrIIGZBt0ukKuOUE1qV6kJ3GgioSOYzsRc8z9Hfr1BVmacA/jLe9nARfmgMGgn68BqIAgg==\",\n+      \"dev\": true,\n+      \"license\": \"(MIT OR Apache-2.0)\",\n+      \"dependencies\": {\n+        \"jsonparse\": \"^1.2.0\",\n+        \"through2\": \"^4.0.2\"\n+      },\n+      \"bin\": {\n+        \"jsonstream-next\": \"bin.js\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=10\"\n+      }\n+    },\n     \"node_modules/jsx-ast-utils\": {\n       \"version\": \"3.3.5\",\n       \"resolved\": \"https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz\",\n@@ -11860,6 +12007,13 @@\n         \"@jridgewell/sourcemap-codec\": \"^1.5.5\"\n       }\n     },\n+    \"node_modules/map-stream\": {\n+      \"version\": \"0.0.7\",\n+      \"resolved\": \"https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz\",\n+      \"integrity\": \"sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\"\n+    },\n     \"node_modules/marked\": {\n       \"version\": \"15.0.12\",\n       \"resolved\": \"https://registry.npmjs.org/marked/-/marked-15.0.12.tgz\",\n@@ -12673,6 +12827,19 @@\n       \"dev\": true,\n       \"license\": \"MIT\"\n     },\n+    \"node_modules/pause-stream\": {\n+      \"version\": \"0.0.11\",\n+      \"resolved\": \"https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz\",\n+      \"integrity\": \"sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==\",\n+      \"dev\": true,\n+      \"license\": [\n+        \"MIT\",\n+        \"Apache2\"\n+      ],\n+      \"dependencies\": {\n+        \"through\": \"~2.3\"\n+      }\n+    },\n     \"node_modules/peberminta\": {\n       \"version\": \"0.9.0\",\n       \"resolved\": \"https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz\",\n@@ -13303,6 +13470,16 @@\n         \"url\": \"https://paulmillr.com/funding/\"\n       }\n     },\n+    \"node_modules/readline-transform\": {\n+      \"version\": \"1.0.0\",\n+      \"resolved\": \"https://registry.npmjs.org/readline-transform/-/readline-transform-1.0.0.tgz\",\n+      \"integrity\": \"sha512-7KA6+N9IGat52d83dvxnApAWN+MtVb1MiVuMR/cf1O4kYsJG+g/Aav0AHcHKsb6StinayfPLne0+fMX2sOzAKg==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"engines\": {\n+        \"node\": \"&gt;=6\"\n+      }\n+    },\n     \"node_modules/reflect.getprototypeof\": {\n       \"version\": \"1.0.10\",\n       \"resolved\": \"https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz\",\n@@ -13347,6 +13524,16 @@\n         \"url\": \"https://github.com/sponsors/ljharb\"\n       }\n     },\n+    \"node_modules/require-directory\": {\n+      \"version\": \"2.1.1\",\n+      \"resolved\": \"https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz\",\n+      \"integrity\": \"sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"engines\": {\n+        \"node\": \"&gt;=0.10.0\"\n+      }\n+    },\n     \"node_modules/require-from-string\": {\n       \"version\": \"2.0.2\",\n       \"resolved\": \"https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz\",\n@@ -14091,6 +14278,19 @@\n         \"source-map\": \"^0.6.0\"\n       }\n     },\n+    \"node_modules/split\": {\n+      \"version\": \"1.0.1\",\n+      \"resolved\": \"https://registry.npmjs.org/split/-/split-1.0.1.tgz\",\n+      \"integrity\": \"sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"through\": \"2\"\n+      },\n+      \"engines\": {\n+        \"node\": \"*\"\n+      }\n+    },\n     \"node_modules/stable-hash\": {\n       \"version\": \"0.0.5\",\n       \"resolved\": \"https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz\",\n@@ -14157,6 +14357,17 @@\n         \"node\": \"&gt;= 0.4\"\n       }\n     },\n+    \"node_modules/stream-combiner\": {\n+      \"version\": \"0.2.2\",\n+      \"resolved\": \"https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz\",\n+      \"integrity\": \"sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"duplexer\": \"~0.1.1\",\n+        \"through\": \"~2.3.4\"\n+      }\n+    },\n     \"node_modules/string_decoder\": {\n       \"version\": \"1.3.0\",\n       \"resolved\": \"https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz\",\n@@ -14167,6 +14378,28 @@\n         \"safe-buffer\": \"~5.2.0\"\n       }\n     },\n+    \"node_modules/string-width\": {\n+      \"version\": \"4.2.3\",\n+      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n+      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"emoji-regex\": \"^8.0.0\",\n+        \"is-fullwidth-code-point\": \"^3.0.0\",\n+        \"strip-ansi\": \"^6.0.1\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=8\"\n+      }\n+    },\n+    \"node_modules/string-width/node_modules/emoji-regex\": {\n+      \"version\": \"8.0.0\",\n+      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz\",\n+      \"integrity\": \"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\"\n+    },\n     \"node_modules/string.prototype.includes\": {\n       \"version\": \"2.0.1\",\n       \"resolved\": \"https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz\",\n@@ -14280,6 +14513,19 @@\n         \"url\": \"https://github.com/sponsors/ljharb\"\n       }\n     },\n+    \"node_modules/strip-ansi\": {\n+      \"version\": \"6.0.1\",\n+      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n+      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"ansi-regex\": \"^5.0.1\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=8\"\n+      }\n+    },\n     \"node_modules/strip-bom\": {\n       \"version\": \"3.0.0\",\n       \"resolved\": \"https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz\",\n@@ -14513,6 +14759,23 @@\n       \"license\": \"MIT\",\n       \"peer\": true\n     },\n+    \"node_modules/through\": {\n+      \"version\": \"2.3.8\",\n+      \"resolved\": \"https://registry.npmjs.org/through/-/through-2.3.8.tgz\",\n+      \"integrity\": \"sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\"\n+    },\n+    \"node_modules/through2\": {\n+      \"version\": \"4.0.2\",\n+      \"resolved\": \"https://registry.npmjs.org/through2/-/through2-4.0.2.tgz\",\n+      \"integrity\": \"sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"readable-stream\": \"3\"\n+      }\n+    },\n     \"node_modules/tinybench\": {\n       \"version\": \"2.9.0\",\n       \"resolved\": \"https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz\",\n@@ -15614,6 +15877,24 @@\n         \"node\": \"&gt;=0.10.0\"\n       }\n     },\n+    \"node_modules/wrap-ansi\": {\n+      \"version\": \"7.0.0\",\n+      \"resolved\": \"https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz\",\n+      \"integrity\": \"sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"ansi-styles\": \"^4.0.0\",\n+        \"string-width\": \"^4.1.0\",\n+        \"strip-ansi\": \"^6.0.0\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=10\"\n+      },\n+      \"funding\": {\n+        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n+      }\n+    },\n     \"node_modules/wrappy\": {\n       \"version\": \"1.0.2\",\n       \"resolved\": \"https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz\",\n@@ -15650,12 +15931,51 @@\n       \"dev\": true,\n       \"license\": \"MIT\"\n     },\n+    \"node_modules/y18n\": {\n+      \"version\": \"5.0.8\",\n+      \"resolved\": \"https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz\",\n+      \"integrity\": \"sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==\",\n+      \"dev\": true,\n+      \"license\": \"ISC\",\n+      \"engines\": {\n+        \"node\": \"&gt;=10\"\n+      }\n+    },\n     \"node_modules/yallist\": {\n       \"version\": \"3.1.1\",\n       \"resolved\": \"https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz\",\n       \"integrity\": \"sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==\",\n       \"license\": \"ISC\"\n     },\n+    \"node_modules/yargs\": {\n+      \"version\": \"17.7.3\",\n+      \"resolved\": \"https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz\",\n+      \"integrity\": \"sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==\",\n+      \"dev\": true,\n+      \"license\": \"MIT\",\n+      \"dependencies\": {\n+        \"cliui\": \"^8.0.1\",\n+        \"escalade\": \"^3.1.1\",\n+        \"get-caller-file\": \"^2.0.5\",\n+        \"require-directory\": \"^2.1.1\",\n+        \"string-width\": \"^4.2.3\",\n+        \"y18n\": \"^5.0.5\",\n+        \"yargs-parser\": \"^21.1.1\"\n+      },\n+      \"engines\": {\n+        \"node\": \"&gt;=12\"\n+      }\n+    },\n+    \"node_modules/yargs-parser\": {\n+      \"version\": \"21.1.1\",\n+      \"resolved\": \"https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz\",\n+      \"integrity\": \"sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==\",\n+      \"dev\": true,\n+      \"license\": \"ISC\",\n+      \"engines\": {\n+        \"node\": \"&gt;=12\"\n+      }\n+    },\n     \"node_modules/yocto-queue\": {\n       \"version\": \"0.1.0\",\n       \"resolved\": \"https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz\",\ndiff --git a/package.json b/package.json\nindex 06f565b..bb87a63 100644\n--- a/package.json\n+++ b/package.json\n@@ -49,6 +49,7 @@\n     \"@types/node\": \"^26\",\n     \"@types/react\": \"^19\",\n     \"@types/react-dom\": \"^19\",\n+    \"audit-ci\": \"^7.1.0\",\n     \"eslint\": \"^9\",\n     \"eslint-config-next\": \"^16.3.0\",\n     \"exceljs\": \"^4.4.0\",\ndiff --git a/src/app/admin/uebernahmen/page.tsx b/src/app/admin/uebernahmen/page.tsx\nindex a0eb564..ce7206e 100644\n--- a/src/app/admin/uebernahmen/page.tsx\n+++ b/src/app/admin/uebernahmen/page.tsx\n@@ -24,7 +24,7 @@ export const metadata = { title: '\u00dcbernahmen \u2014 Admin \u2014 pledform', robots: {\n const FONT = 'var(--font-inter), sans-serif'\n \n type UebernahmeMitInstitution = InstitutionUebernahme &amp; {\n-  institution: { id: string; name: string | null; institutions_id: string | null; inhaber_id: string | null; quelle: string | null } | null\n+  institution: { id: string; name: string | null; institutions_id: string | null; inhaber_id: string | null; quelle: string | null; status: string | null } | null\n }\n \n function StatusBadge({ status }: { status: UebernahmeStatus }) {\n@@ -65,7 +65,7 @@ export default async function AdminUebernahmenListe({\n \n   let query = supabaseAdmin\n     .from('institution_uebernahmen')\n-    .select('*, institution:institutionen(id, name, institutions_id, inhaber_id, quelle)')\n+    .select('*, institution:institutionen(id, name, institutions_id, inhaber_id, quelle, status)')\n     .order('erstellt_am', { ascending: false })\n     .limit(200)\n   if (filter !== 'alle') query = query.eq('status', filter)\n@@ -171,9 +171,14 @@ export default async function AdminUebernahmenListe({\n                 )}\n \n                 {a.status === 'eingereicht' ? (\n-                  a.institution?.inhaber_id ? (\n+                  a.institution?.status === 'Archiviert' ? (\n                     \n-                      Achtung: Die Institution hat inzwischen einen Inhaber \u2014 nur Ablehnen m\u00f6glich.\n+                      Achtung: Dieses Anbieterprofil ist archiviert. Eine Genehmigung ist nicht\n+                      m\u00f6glich, nur Ablehnen. Wer das Profil zur\u00fcckholen will, reaktiviert es zuerst.\n+                    \n+                  ) : a.institution?.inhaber_id ? (\n+                    \n\n+                      Achtung: Das Anbieterprofil hat inzwischen einen Inhaber. Nur Ablehnen m\u00f6glich.\n                     \n                   ) : null\n                 ) : (\n@@ -183,7 +188,17 @@ export default async function AdminUebernahmenListe({\n                   \n                 )}\n \n-                {a.status === 'eingereicht' &amp;&amp; }\n+                {a.status === 'eingereicht' &amp;&amp; (\n+                  \n+                )}\n               \n             )\n           })}\ndiff --git a/src/components/admin/UebernahmeAdminAktionen.tsx b/src/components/admin/UebernahmeAdminAktionen.tsx\nindex 6c5b811..8247b17 100644\n--- a/src/components/admin/UebernahmeAdminAktionen.tsx\n+++ b/src/components/admin/UebernahmeAdminAktionen.tsx\n@@ -29,7 +29,22 @@ const knopfBasis: React.CSSProperties = {\n   cursor: 'pointer',\n }\n \n-export default function UebernahmeAdminAktionen({ uebernahmeId }: { uebernahmeId: string }) {\n+export default function UebernahmeAdminAktionen({\n+  uebernahmeId,\n+  genehmigenMoeglich = true,\n+}: {\n+  uebernahmeId: string\n+  /**\n+   * false, wenn die RPC eine Genehmigung ohnehin abweisen w\u00fcrde (Institution\n+   * inzwischen vergeben \u21d2 UEB02, oder archiviert \u21d2 UEB04). Dann verschwindet\n+   * der Knopf; Ablehnen bleibt.\n+   *\n+   * Bewusst NICHT: den Claim automatisch schlie\u00dfen. Ein Endstatus w\u00e4re eine\n+   * Entscheidung, die niemand getroffen hat \u2014 eine erfundene Entscheidung im\n+   * Moderationsprotokoll ist schlimmer als eine offene Anfrage.\n+   */\n+  genehmigenMoeglich?: boolean\n+}) {\n   const [notiz, setNotiz] = useState('')\n   const [laeuft, setLaeuft] = useState(false)\n   const [fehler, setFehler] = useState(null)\n@@ -66,10 +81,12 @@ export default function UebernahmeAdminAktionen({ uebernahmeId }: { uebernahmeId\n       /&gt;\n \n       \n\n-         entscheiden('genehmigt')}\n-          style={{ ...knopfBasis, backgroundColor: '#4A7C59', color: '#FFFFFF', border: 'none', opacity: laeuft ? 0.6 : 1 }}&gt;\n-          Genehmigen (Ownership + Badge wechseln)\n-        \n+        {genehmigenMoeglich &amp;&amp; (\n+           entscheiden('genehmigt')}\n+            style={{ ...knopfBasis, backgroundColor: '#4A7C59', color: '#FFFFFF', border: 'none', opacity: laeuft ? 0.6 : 1 }}&gt;\n+            Genehmigen (Ownership + Badge wechseln)\n+          \n+        )}\n          entscheiden('abgelehnt')}\n           style={{ ...knopfBasis, backgroundColor: '#FFFFFF', color: '#A83333', border: '1px solid #F4D6D6', opacity: laeuft ? 0.6 : 1 }}&gt;\n           Ablehnen\ndiff --git a/src/lib/utils/audit-allowlist.test.ts b/src/lib/utils/audit-allowlist.test.ts\nnew file mode 100644\nindex 0000000..fc994b1\n--- /dev/null\n+++ b/src/lib/utils/audit-allowlist.test.ts\n@@ -0,0 +1,70 @@\n+// W\u00e4chter \u00fcber audit-ci.jsonc.\n+// ---------------------------------------------------------------------------\n+// Eine Allowlist ohne Ablaufkontrolle ist eine stillgelegte Sicherung: Der\n+// Eintrag bleibt, der Grund verschwindet, und niemand merkt es. Dieser Test\n+// macht aus dem Verfallsdatum im Kommentar eine echte Bedingung \u2014 l\u00e4uft ein\n+// Eintrag ab, wird CI rot und jemand muss entscheiden.\n+//\n+// Zus\u00e4tzlich: Die Allowlist muss GHSA-genau bleiben. Ein paketweiter Eintrag\n+// (\"next\") w\u00fcrde jedes zuk\u00fcnftige Advisory desselben Pakets verschlucken.\n+\n+import { readFileSync } from 'node:fs'\n+import { join } from 'node:path'\n+import { describe, expect, it } from 'vitest'\n+\n+const PFAD = join(process.cwd(), 'audit-ci.jsonc')\n+const ROH = readFileSync(PFAD, 'utf8')\n+\n+/** Minimaler jsonc-Reader: entfernt Zeilenkommentare au\u00dferhalb von Strings. */\n+function ohneKommentare(text: string): string {\n+  return text\n+    .split('\\n')\n+    .map((zeile) =&gt; {\n+      let inString = false\n+      for (let i = 0; i &lt; zeile.length; i++) {\n+        if (zeile[i] === '\"' &amp;&amp; zeile[i - 1] !== '\\\\') inString = !inString\n+        if (!inString &amp;&amp; zeile[i] === '/' &amp;&amp; zeile[i + 1] === '/') return zeile.slice(0, i)\n+      }\n+      return zeile\n+    })\n+    .join('\\n')\n+}\n+\n+const KONFIG = JSON.parse(ohneKommentare(ROH)) as {\n+  high?: boolean\n+  critical?: boolean\n+  allowlist?: string[]\n+}\n+\n+/** Alle \u201eVerf\u00e4llt: YYYY-MM-TT\"-Marken aus den Kommentaren. */\n+const VERFALLSDATEN = [...ROH.matchAll(/Verf\u00e4llt:\\s*(\\d{4}-\\d{2}-\\d{2})/g)].map((m) =&gt; m[1])\n+\n+describe('audit-ci.jsonc', () =&gt; {\n+  it('gilt f\u00fcr high und critical (Schwelle wie beim npm-audit-Vorg\u00e4nger)', () =&gt; {\n+    expect(KONFIG.high).toBe(true)\n+    expect(KONFIG.critical).toBe(true)\n+  })\n+\n+  it('f\u00fchrt ausschlie\u00dflich GHSA-genaue Eintr\u00e4ge, nie paketweite', () =&gt; {\n+    const eintraege = KONFIG.allowlist ?? []\n+    expect(eintraege.length).toBeGreaterThan(0)\n+    for (const eintrag of eintraege) {\n+      expect(eintrag, `\u201e${eintrag}\" ist keine GHSA-ID`).toMatch(/^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/)\n+    }\n+  })\n+\n+  it('belegt jede Gruppe mit einem Verfallsdatum', () =&gt; {\n+    expect(VERFALLSDATEN.length).toBeGreaterThan(0)\n+  })\n+\n+  it('hat keinen abgelaufenen Eintrag', () =&gt; {\n+    const heute = new Date().toISOString().slice(0, 10)\n+    const abgelaufen = VERFALLSDATEN.filter((datum) =&gt; datum &lt; heute)\n+    expect(\n+      abgelaufen,\n+      `Allowlist-Eintrag abgelaufen (${abgelaufen.join(', ')}). Entweder ist das ` +\n+        'next-Upgrade jetzt m\u00f6glich (dann Eintr\u00e4ge raus), oder das Datum wird mit ' +\n+        'neuer Begr\u00fcndung verl\u00e4ngert. Nicht kommentarlos weiterschieben.',\n+    ).toEqual([])\n+  })\n+})\ndiff --git a/supabase/migrations/20260814120000_next_institutions_id_grants.sql b/supabase/migrations/20260814120000_next_institutions_id_grants.sql\nnew file mode 100644\nindex 0000000..3f2c739\n--- /dev/null\n+++ b/supabase/migrations/20260814120000_next_institutions_id_grants.sql\n@@ -0,0 +1,74 @@\n+-- =============================================================================\n+-- Nachz\u00fcgler \u2014 EXECUTE-H\u00e4rtung: next_institutions_id + zwei Drift-F\u00e4lle\n+-- =============================================================================\n+-- ANLASS: Advisor-Bestandsmeldung \u2014 public.next_institutions_id() ist SECURITY\n+-- DEFINER und f\u00fcr `anon` ausf\u00fchrbar. Ein anonymer Aufruf kann keine Zeile\n+-- schreiben, verbrennt aber Sequenz-Nummern und legt die Vergabe offen.\n+--\n+-- WICHTIG, sonst wirkt die Migration nicht: Das Recht kommt NICHT aus einem\n+-- Grant an anon, sondern aus dem Postgres-Default an PUBLIC. Prod-ACL:\n+--     next_institutions_id \u2192 \"=X/postgres | postgres=X/postgres | service_role=X/postgres\"\n+--                             ^^^ das f\u00fchrende \"=X\" IST PUBLIC.\n+-- Ein `revoke execute ... from anon` allein bliebe folgenlos, weil anon das\n+-- Recht \u00fcber PUBLIC behielte. Deshalb wird von PUBLIC entzogen; anon und\n+-- authenticated stehen nur der Deutlichkeit halber mit dabei.\n+--\n+-- BELEG, dass niemand die Funktion braucht (Stand 10.08.2026):\n+--   1. `grep -rn \"rpc('next_\" src/ scripts/` liefert GENAU eine Zeile:\n+--        src/lib/actions.ts:768  supabaseAdmin.rpc('next_institutions_id')\n+--      -&gt; service_role, nicht anon/authenticated.\n+--   2. KEIN Spalten-Default verweist auf sie. Das unterscheidet sie von allen\n+--      Geschwistern und ist der Grund, warum hier auch `authenticated` gehen\n+--      kann: ein Default w\u00fcrde mit den Rechten der EINF\u00dcGENDEN Rolle\n+--      ausgewertet, ein Entzug br\u00e4che dann die Anlage still.\n+--\n+-- INVENTAR der Familie, gegen Kollateralschaden gepr\u00fcft:\n+--   Funktion                  Default auf         authenticated  Entscheidung\n+--   next_abrechnung_id        abrechnungen        n\u00f6tig          bleibt\n+--   next_antrag_id            antraege            n\u00f6tig          bleibt\n+--   next_buchungsanfrage_id   buchungsanfragen    n\u00f6tig          bleibt\n+--   next_buchungsvorgang_id   buchungsvorgaenge   n\u00f6tig          bleibt\n+--   next_org_id               organisationen      n\u00f6tig          bleibt\n+--   next_proj_id              projekte            nicht n\u00f6tig    siehe (2)\n+--   next_institutions_id      \u2014 (keiner)          nicht n\u00f6tig    ENTZUG (1)\n+--   starte_konversation       \u2014                   legitim        bleibt, siehe (3)\n+--   (\u201en\u00f6tig\" = authenticated hat auf der Tabelle INSERT und l\u00f6st damit den\n+--   Spalten-Default aus. Bei projekte hat authenticated gar kein INSERT.)\n+--\n+-- ZWEI DRIFT-F\u00c4LLE, beim Inventarisieren aufgefallen (Teile 2 und 3):\n+-- next_proj_id und starte_konversation sind in PROD geh\u00e4rtet, im REPO aber\n+-- nicht \u2014 die H\u00e4rtung wurde damals ad hoc gesetzt, nie als Migration. Lokal\n+-- und in den Tests h\u00e4ngt dort deshalb bis heute das PUBLIC-Default. Beide\n+-- Anweisungen unten sind in Prod folgenlose No-ops und stellen ausschlie\u00dflich\n+-- Repo, lokalen Stack und CI auf denselben Stand (AGENTS.md, GRANT-Disziplin).\n+--\n+-- NICHTS DESTRUKTIVES: nur GRANT-R\u00fccknahme, kein DDL an Daten.\n+-- =============================================================================\n+\n+\n+-- \u2500\u2500 (1) next_institutions_id: nur noch service_role \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n+revoke execute on function public.next_institutions_id() from public, anon, authenticated;\n+\n+comment on function public.next_institutions_id() is\n+  'Vergibt die n\u00e4chste institutions_id. SECURITY DEFINER, aber NUR f\u00fcr service_role '\n+  'ausf\u00fchrbar (Migration 20260814120000): einziger Aufrufer ist erstelleInstitution '\n+  '\u00fcber supabaseAdmin. Anders als die Geschwister next_*_id h\u00e4ngt an dieser Funktion '\n+  'KEIN Spalten-Default \u2014 deshalb braucht auch authenticated sie nicht. '\n+  'Bewacht von supabase/tests/259-next-id-grants.sql.';\n+\n+\n+-- \u2500\u2500 (2) next_proj_id: Prod-Stand nachziehen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n+-- Prod-ACL \"postgres=X | service_role=X\" (kein PUBLIC, kein authenticated).\n+-- Passt dazu, dass authenticated auf projekte gar kein INSERT hat, der\n+-- Spalten-Default also nie \u00fcber diese Rolle ausgel\u00f6st wird.\n+revoke execute on function public.next_proj_id() from public, anon, authenticated;\n+\n+\n+-- \u2500\u2500 (3) starte_konversation: PUBLIC raus, authenticated ausdr\u00fccklich rein \u2500\u2500\u2500\n+-- Prod-ACL \"postgres=X | authenticated=X | service_role=X\". Die Funktion wird\n+-- von authenticated legitim gerufen \u2014 das Recht muss also explizit stehen,\n+-- statt sich aus dem PUBLIC-Default zu ergeben (sonst h\u00e4tte anon es mit).\n+revoke execute on function public.starte_konversation(text, uuid, text, uuid, text)\n+  from public, anon;\n+grant  execute on function public.starte_konversation(text, uuid, text, uuid, text)\n+  to authenticated;\ndiff --git a/supabase/tests/259-next-id-grants.sql b/supabase/tests/259-next-id-grants.sql\nnew file mode 100644\nindex 0000000..d955199\n--- /dev/null\n+++ b/supabase/tests/259-next-id-grants.sql\n@@ -0,0 +1,89 @@\n+-- =============================================================================\n+-- 259-next-id-grants.sql \u2014 GUARD: EXECUTE-Matrix der next_*_id-Funktionen\n+-- =============================================================================\n+-- Bewacht Migration 20260814120000 (EXECUTE auf next_institutions_id entzogen)\n+-- und schreibt zugleich die ganze Familie fest. Muster wie 009, aber mit einem\n+-- Zusatz, den 009 nicht braucht:\n+--\n+-- Bei sechs der sieben Funktionen h\u00e4ngt ein SPALTEN-DEFAULT an der Funktion.\n+-- Ein Default wird mit den Rechten der EINF\u00dcGENDEN Rolle ausgewertet \u2014 wer dort\n+-- `authenticated` das EXECUTE entzieht, bricht die Anlage still. Deshalb pr\u00fcft\n+-- diese Datei nicht nur die Grants, sondern auch die Kopplung dahinter:\n+-- \u201eDefault vorhanden UND authenticated darf INSERT\" \u21d2 EXECUTE muss bleiben.\n+-- F\u00e4llt eine dieser Voraussetzungen k\u00fcnftig weg, wird der Test rot und jemand\n+-- entscheidet bewusst, statt dass ein Grant grundlos weiterlebt.\n+-- =============================================================================\n+begin;\n+\n+select plan(20);\n+\n+-- ---- Der Kern: next_institutions_id ist NUR noch service_role ---------------\n+select ok(not has_function_privilege('anon',          'public.next_institutions_id()', 'EXECUTE'),\n+  'anon !EXECUTE next_institutions_id');\n+select ok(not has_function_privilege('authenticated', 'public.next_institutions_id()', 'EXECUTE'),\n+  'authenticated !EXECUTE next_institutions_id (einziger Aufrufer ist supabaseAdmin)');\n+select ok(    has_function_privilege('service_role',  'public.next_institutions_id()', 'EXECUTE'),\n+  'service_role EXECUTE next_institutions_id');\n+\n+-- Die Begr\u00fcndung selbst absichern: sobald ein Spalten-Default auf diese Funktion\n+-- zeigt, w\u00e4re der authenticated-Entzug falsch (Anlage \u00fcber die Rolle br\u00e4che).\n+select is(\n+  (select count(*)::int from pg_attrdef d\n+    join pg_class c on c.oid = d.adrelid\n+    join pg_namespace n on n.oid = c.relnamespace and n.nspname = 'public'\n+   where pg_get_expr(d.adbin, d.adrelid) like '%next_institutions_id%'),\n+  0,\n+  'next_institutions_id h\u00e4ngt an KEINEM Spalten-Default (Voraussetzung des Entzugs)');\n+\n+-- ---- anon hat auf KEINER der sieben etwas verloren --------------------------\n+select ok(not has_function_privilege('anon', 'public.next_abrechnung_id()', 'EXECUTE'),      'anon !EXECUTE next_abrechnung_id');\n+select ok(not has_function_privilege('anon', 'public.next_antrag_id()', 'EXECUTE'),          'anon !EXECUTE next_antrag_id');\n+select ok(not has_function_privilege('anon', 'public.next_buchungsanfrage_id()', 'EXECUTE'), 'anon !EXECUTE next_buchungsanfrage_id');\n+select ok(not has_function_privilege('anon', 'public.next_buchungsvorgang_id()', 'EXECUTE'), 'anon !EXECUTE next_buchungsvorgang_id');\n+select ok(not has_function_privilege('anon', 'public.next_org_id()', 'EXECUTE'),             'anon !EXECUTE next_org_id');\n+select ok(not has_function_privilege('anon', 'public.next_proj_id()', 'EXECUTE'),            'anon !EXECUTE next_proj_id');\n+\n+-- ---- Die f\u00fcnf Default-gebundenen BEHALTEN authenticated ---------------------\n+-- Jeweils mit der Kopplung, die den Grant rechtfertigt: Default + INSERT-Recht.\n+select ok(has_function_privilege('authenticated', 'public.next_abrechnung_id()', 'EXECUTE')\n+      and has_table_privilege('authenticated', 'public.abrechnungen', 'INSERT'),\n+  'next_abrechnung_id: authenticated EXECUTE n\u00f6tig (Default auf abrechnungen + INSERT)');\n+select ok(has_function_privilege('authenticated', 'public.next_antrag_id()', 'EXECUTE')\n+      and has_table_privilege('authenticated', 'public.antraege', 'INSERT'),\n+  'next_antrag_id: authenticated EXECUTE n\u00f6tig (Default auf antraege + INSERT)');\n+select ok(has_function_privilege('authenticated', 'public.next_buchungsanfrage_id()', 'EXECUTE')\n+      and has_table_privilege('authenticated', 'public.buchungsanfragen', 'INSERT'),\n+  'next_buchungsanfrage_id: authenticated EXECUTE n\u00f6tig (Default + INSERT)');\n+select ok(has_function_privilege('authenticated', 'public.next_buchungsvorgang_id()', 'EXECUTE')\n+      and has_table_privilege('authenticated', 'public.buchungsvorgaenge', 'INSERT'),\n+  'next_buchungsvorgang_id: authenticated EXECUTE n\u00f6tig (Default + INSERT)');\n+select ok(has_function_privilege('authenticated', 'public.next_org_id()', 'EXECUTE')\n+      and has_table_privilege('authenticated', 'public.organisationen', 'INSERT'),\n+  'next_org_id: authenticated EXECUTE n\u00f6tig (Default auf organisationen + INSERT)');\n+\n+-- Die Defaults, auf die sich diese f\u00fcnf Grants st\u00fctzen, m\u00fcssen auch existieren.\n+select is(\n+  (select count(distinct c.relname)::int\n+     from pg_attrdef d\n+     join pg_class c on c.oid = d.adrelid\n+     join pg_namespace n on n.oid = c.relnamespace and n.nspname = 'public'\n+    where c.relname in ('abrechnungen','antraege','buchungsanfragen','buchungsvorgaenge','organisationen')\n+      and pg_get_expr(d.adbin, d.adrelid) like 'next\\_%\\_id()'),\n+  5,\n+  'die f\u00fcnf Spalten-Defaults existieren (Grundlage der f\u00fcnf Grants oben)');\n+\n+-- ---- next_proj_id: strengster Fall, bleibt streng ---------------------------\n+select ok(not has_function_privilege('authenticated', 'public.next_proj_id()', 'EXECUTE'),\n+  'next_proj_id: authenticated !EXECUTE');\n+select ok(not has_table_privilege('authenticated', 'public.projekte', 'INSERT'),\n+  'next_proj_id passt dazu: authenticated hat auf projekte gar kein INSERT');\n+\n+-- ---- starte_konversation bleibt f\u00fcr authenticated erreichbar ----------------\n+select ok(has_function_privilege('authenticated',\n+  'public.starte_konversation(text,uuid,text,uuid,text)', 'EXECUTE'),\n+  'starte_konversation: authenticated EXECUTE bleibt (legitimer Client-Aufruf)');\n+select ok(not has_function_privilege('anon',\n+  'public.starte_konversation(text,uuid,text,uuid,text)', 'EXECUTE'),\n+  'starte_konversation: anon !EXECUTE');\n+\n+rollback;\n", "creation_timestamp": "2026-08-10T19:39:37.981104Z"}