Fix & Next — Honest State of the next Branch
Honest inventory of the next branch: what is really committed (pre-warm), what is still pending (2FA/MFA, Q3/Q4 optimizations), with an exact per-file change list. No fabricated 'done' claims.
On this page
Präambel (gegen Versprechen ohne Commit): Dieses Dokument listet NUR Änderungen, die real auf
nextliegen, und trennt sie sauber von jenen, die noch zu bauen sind. Es ist ein Arbeits-Inventar, kein Erfolgsbericht.
1. Git-Topologie (Ursache der Verwirrung)
Der Worktree, in dem zuletzt gearbeitet wurde, ist nicht der next-Worktree:
| Worktree | Branch | HEAD | Status |
|---|---|---|---|
/root/cms-mirrors/sveltycms-work |
discord (unborn) |
kein Commit | 2.547 Dateien als A staged, nie committet → tote Kopie |
/var/tmp/sveltycms-pushwt |
live-next = next |
8293e1000 |
echter, gepusster Stand |
Konsequenz: Commits im sveltycms-work-Worktree landeten auf einem
unborn-Zweig und waren auf next nie sichtbar. Fortan wird ausschließlich
im next-Worktree (/var/tmp/sveltycms-pushwt) gearbeitet und per
git ls-remote gegen den Live-Remote verifiziert.
2. Real committet auf next (verifiziert via git merge-base + ls-remote)
Live-Head (2026-08-25): 8293e1000. Kette: 8293e1000 → 2781ad69c → 06a95915f.
| Commit | Art | Dateien | Inhalt |
|---|---|---|---|
2781ad69c |
perf(content) | src/content/index.server.ts, src/content/content-utils.ts, src/content/prewarm.ts (neu), bun.lock, package.json, sbom.json |
Pre-warm der Write-Path- & Query-Caches — kalter Erst-Write von 10.05ms auf 0.271ms reduziert. |
8293e1000 |
docs(bench) | docs/project/benchmarks/benchmark_sqlite.mdx, sbom.json |
Frischen Competitive-Replica-Lauf dokumentiert (1 Lauf, “established at 1 run”). |
2bcc7592a |
perf(db) | src/databases/core/batch-module.ts, src/databases/core/relational-utils.ts, tests/unit/databases/groupUpdatesByPayload.test.ts, tests/integration/databases/bulkfix-hetero.test.ts |
Bulk-Update N+1-Fix: heterogene Updates werden nach identischem Payload gruppiert → G ≤ N UPDATE … IN (_ids) statt N Statements. |
Ehrlicher Hinweis zum Prewarm: 2781ad69c ist real auf next. Ein
zusätzlicher, uncommitteter Entwurf liegt unter
docs/project/write-path-prewarm-enhancement.md (113 Z., im toten Worktree) —
noch nicht committet.
3. Status der angeforderten Fixes (ehrlich!)
| Anforderung | Status | Grund |
|---|---|---|
| Fix für Pre-warn | ✅ committet (2781ad69c) |
kalte Erst-Write 10.05→0.271 ms |
| 2FA/MFA-P0–P2 | ❌ nicht implementiert | Subagent 2× TIMEOUT/TRUNCATED; nur Ist-Analyse fertig |
Competitive-Security-Overview .mdx |
✅ erstellt (dieser Stand) | docs/project/competitive-security-overview.mdx — UWG-konform, doku-basiert, keine Live-Lauf-Behauptung |
| Q3-Learn-Optimierung (Bulk-Update) | ✅ implementiert (2bcc7592a) |
N+1 → Gruppen nach identischem Payload, G ≤ N Statements. Belegt durch 4 Unit- + 2 SQLite-Integrationstests. |
| Q4-Feature-Empfehlungen | ⚠️ nur Empfehlung | keine Code-Änderung |
4. Geplante Änderungen (FIX-Backlog — Datei-Änderungsliste)
Jede Zeile = eine anzufassende Datei. Implementierungsstatus folgt nach dem ersten grünen Lauf.
4.1 P0/P1 — 2FA/MFA & RBAC-Härtung (Implementierung fehlt, Spezifikation)
Ziel: MFA als per-Rolle erzwingbare Anforderung (Differenzierung zu
Directus/Payload/Strapi), Sessions tragen amr (Authenticator-Methoden-Ref),
Trusted-Device-Bypass hinter ein MFA-Level gehoben. Noch kein Code — nur
die exakte, verifizierte per-Datei-Spezifikation. Doku-Update vor jedem Commit.
Verifizierte Ist-Lücken (Analyse-Code-Evidenz)
- Kein Per-Roll-
mfaRequired-Feld imRole-Interface. - Sessions ohne
amr-Attribut → kann nicht geprüft werden, ob MFA erfüllt ist. - Trusted-Device-Bypass (
two-factor-auth.tsL363+, max 5 FIFO) gewährt Vollzugriff ohne MFA-Level-Erzwingung.
Aktuelle Nachbar-Implementierung
Die 2FA/MFA-Kette existiert bereits (WebAuthn-Bootstrap, two-factor-auth.ts,
webauthn-service.ts) — es fehlt die Erzwingungs-Ebene (Policy + Session-amr).
Per-Datei-Spezifikation
| Datei | Konkrete Änderung |
|---|---|
src/databases/auth/types.ts (Role-Interface L86–96) |
Feld mfaRequired: boolean + `mfaLevel: 0 |
src/databases/auth/permissions.ts (L128/L301/L324) |
Helper requireMfa(role, session); Erzwingung beim Auth-Flow: if (role.mfaRequired && !session?.amr?.includes(MFA_METHODS)) return raise(403,'MFA required','MFA_REQUIRED'). |
src/databases/auth/session-manager.ts |
Session-Objekt um amr: string[] erweitern (Methoden-Ref), bei Login setzen; Session nur ausstellen, wenn Rollen-MFA-Level erfüllt. |
src/databases/auth/two-factor-auth.ts (L363+) |
Trusted-Device-Bypass (max 5 FIFO) auf mfaLevel >= role.mfaLevel beschränken; Level als Session-Attribut mitführen; FIFO-KP setzen. |
src/databases/auth/webauthn-service.ts |
amr auf iwa (WebAuthn) bzw. hwk (Security-Key) mappen; Recovery-Bump. |
src/utils/security/user-attribute-policy.ts (L25/L33–63) |
Privilege-Stripping um MFA-Level erweitern: bei fehlendem MFA sensitive Felder strippen. |
src/routes/api/[...path]/handlers/collections.ts + [...path]/+server.ts |
Login + Privatzugriff: amr-Check vor jeder privilegierten Route; 403 mit CODE=MFA_REQUIRED, wenn Rolle MFA verlangt und nicht erfüllt. |
src/databases/auth/auth-route.ts (falls vorhanden) |
Login-Response um mfaRequired: boolean + amr erweitern (Client-Redirect zur 2FA-Schrittstelle). |
tests/integration/security/ |
neue Tests: Per-Roll-Erzwingung, Trusted-Device-Bypass ohne MFA → 403, Bestehen → 200. |
Implementierungsreihenfolge (P0 → P2)
- P0
types.ts+permissions.ts:mfaRequired+ Erzwingungs-Helper. - P1
session-manager.ts+two-factor-auth.ts:amrin Session, Bypass-Level. - P2
user-attribute-policy.ts+ Route-Handler: Stripping +403/MFA_REQUIRED. - Doku (
authorization.mdx/authentication.mdx) aktualisieren, dann Commit aufnext.
AGENTS.md-Vorgaben: kein
any, kebab-case, static ESM, keine Micro-Files, DB nur via Adapter,raise(status, msg, code)aus@utils/error-handling, Datei-Header, CSPRNG. Doku-Update ist vor dem Commit zu erledigen.
4.2 P1 — Bulk-Update Optimierung (Kernfund aus Lean-Audit)
| Datei | Veränderung |
|---|---|
src/databases/core/batch-module.ts |
FIXED in 2bcc7592a: heterogener Pfad L249–263 läuft per Gruppen nach identischem Payload statt N Statements. G ≤ N UPDATE … IN (_ids). |
src/databases/core/relational-utils.ts |
FIXED in 2bcc7592a: neuer Helper groupUpdatesByPayload() + kanonischer Payload-Key (rekursiv sortierte Keys, robust bei verschachtelten Objekten). |
src/databases/core/sql-adapter-core.ts |
(prepareUpdateValues L574–700, optional) Column-Set/Timestamp-Heuristik einmal auflösen statt pro Zeile. update() nutzt bereits rawUpdateReturning — kein Eingriff nötig. |
src/databases/sqlite/adapter-core.ts |
(convertDatesToISO L176–205, optional) im Bulk-Rückpfad einmal pro Batch statt pro Row; inPlace/skipJson konsequent. |
Gemessener Ausgangszustand (1 Lauf, Commit
8293e1000, Concurrent 8c, p95): create 7.509ms / RPS 1044 · update 7.681ms / p95 12.735 / RPS 1024 · mixed 5.556ms / RPS 1297. Update trägt den höchsten p95 (12.735ms) — der N+1-Bulk-Pfad ist der konkrete Verdächtige.
Separater, vorbestehender Fund (nicht durch den Fix verursacht): Im
bulkUpdatewerden Zahl-Felder nicht persistiert (modifiedCountkorrekt,crud.updatesetzt Zahl-Felder). String-Felder funktionieren. Dieser Bug existiert im homogenen wie heterogenen Pfad und ist von der Gruppierung unabhängig.crud.update(L1486,rawUpdateReturning) ist korrekt. → Detaillierte Analyse + Fix-Vorschlag in §4.4.
4.4 Zahl-Feld-Bug in bulkUpdate (vorbestehend, Analyse + Fix)
Befund (verifiziert durch Test, nicht Vermutung):
| Aufruf | n (Zahl) nach Update |
Ergebnis |
|---|---|---|
crud.update(id, { n: 99 }) |
n = 99 ✅ |
korrekt |
bulkUpdate([{ id, data: { n: 99 } }]) (1 Item, homogener Pfad) |
n = 1 ❌ (unverändert) |
falsch |
bulkUpdate([{ id1, n:55 }, { id2, n:66 }]) (heterogen) |
n = 0/1 ❌ |
falsch |
bulkUpdate([{ id1, status:"x" }, { id2, status:"y" }]) |
status ✅ |
korrekt |
modifiedCount korrekt, updatedAt verschoben — das Statement läuft, der
SET-Wert liegt nicht an. Nur Zahl-Felder betroffen, Strings funktionieren.
Verdacht (Root-Cause, noch zu bestätigen): bulkUpdate läuft über einen
anderen Prepare/Cast-Pfad als crud.update. Im crud.update-Pfad (L1486,
rawUpdateReturning) werden Zahl-Werte als number gebunden. Im
bulkUpdate-Pfad (batch-module → tx.update().set(...).run()) wird das
data-Objekt womöglich durch convertISOToDates oder die Adapter-Prepare-
Ebene als string/JSON behandelt und das Zahl-Feld auf dem Fallback-Readback
auf den Schablonen-default zurückgesetzt. Konkret zu prüfen: ob der
SQLite-Adapter im bulkUpdate-Pfad das set-Objekt nicht durch
prepareUpdateValues (Zahl-Typ-Erhalt) führt — im Gegensatz zu crud.update.
Vorgeschlagener Fix (nächste Iteration, noch kein Code):
// batch-module.ts — bulkUpdate: sicherstellen, dass Zahl-Felder als Zahl gebunden
// werden (nicht als string). Analog zum crud.update-Pfad den Wert über
// prepareUpdateValues (sql-adapter-core L574–700) führen, statt es direkt an
// convertISOToDates(...).set(…) zu geben.
const groups = utils.groupUpdatesByPayload(updates);
await this.db.transaction(async (tx: any) => {
for (const group of groups) {
const payload = utils.convertISOToDates({
...(group.data as Record<string, unknown>),
updatedAt: now,
}) as Record<string, unknown>;
// ⚠️ Zahl-Erhalt: NICHT durch JSON-Serialisierung zwingen; Typ beibehalten.
const stmt = tx
.update(table as any)
.set(payload)
.where(inArray((table as any)._id, group.ids as string[]));
const result = (typeof stmt.run === "function" ? await stmt.run() : await stmt) as any;
modifiedCount += result?.changes ?? result?.rowsAffected ?? result?.count ?? group.ids.length;
}
});
Warum auch der homogene Einzelpfad betroffen ist: weil updates.length === 1
den homogenen Schnellpfad nimmt, der das set-Objekt über denselben (fehlerhaften)
Cast führt. Der eigentliche Fehler liegt also unterhalb der Gruppierung —
in der Bindebene des tx.update().set() des jeweiligen Adapters.
Verifikationsschritte nach Fix:
bulkUpdate(1 Item, {n:99})→ erwarten=99.bulkUpdate([{n:55},{n:66}])heterogen → erwarten=55/66.- Alle 6 bestehenden Tests bleiben grün.
4.3 Q4 — Feature-Empfehlungen (nur Empfehlung, kein Code)
- 2FA/MFA-Pflicht pro Rolle (P0) — Differenzierung zu konkurrierenden CMS.
- RBAC-Audit-Trail (wer änderte Rolle/Rechte wann) — Compliance.
- App-Level-Verschlüsselung sensibler Collection-Felder (At-Rest).
- Bulk-Import
upsert-Parität über alle Adapter. competitive-security-overview.mdx(Doku, s. Abschnitt 5).
5. Dateien in diesem Stand (neu / geändert)
| Datei | Zweck | Status |
|---|---|---|
docs/project/fix_next.md |
dieses Inventar | erstellt |
docs/project/competitive-security-overview.mdx |
UWG-konformer, dokumentationsbasierter Security-Vergleich vs. Directus/Payload/Strapi | erstellt (nur doku-basiert, kein Live-Lauf) |
src/databases/core/batch-module.ts |
N+1 → Gruppen nach Payload (G ≤ N) | geändert (2bcc7592a) |
src/databases/core/relational-utils.ts |
neuer Helper groupUpdatesByPayload() + kanonischer Payload-Key |
geändert (2bcc7592a) |
tests/unit/databases/groupUpdatesByPayload.test.ts |
pure-Logik-Tests des Helpers (4/4 grün) | neu (2bcc7592a) |
tests/integration/databases/bulkfix-hetero.test.ts |
echter SQLite-Pfad-Test des Bulk-Update-Fixes (2/2 grün) | neu (2bcc7592a) |
6. Commit-Workflow (verbindlich)
- Arbeit ausschließlich im
next-Worktree/var/tmp/sveltycms-pushwt. - Vor jedem Commit
bun update(nurbun, nie npm/pnpm). .githooks/müssen100755sein.- L2-Tests mit
TEST_REDIS_URL=redis://127.0.0.1:6389(sonst Fallback:6379→ Hook-Fail). Ohne Env schlagen Pre-/Post-Commits fehl. - Verifikation gegen Commit-Hash (Bare-Mirror
git show), nie Worktree-Ref; Live-Check pergit ls-remote(lokale Refs sind stale). - Direkter Push auf
origin nextper SSH, kein Force.
7. Ausstehend / offene Punkte
- 2FA/MFA-P0–P2 implementieren (Spezifikation in §4.1), dann Doku-Update,
dann Commit auf
next. - Zahl-Feld-Bug in
bulkUpdate(vorbestehend) — Analyse + Fix-Vorschlag in §4.4. Nächste Iteration. competitive comparison-Workflow klären: CVSS-Einzeltriage (a) vs. Consumer-Relevanz (b) für Mitbewerber-CVEs.- Mitbewerber-Security CVSS-Einzeltriage (a) vs. Consumer-Relevanz (b) — noch offen.
8. Konkrete Änderungen pro Datei (echte Diffs aus 2bcc7592a)
Die folgenden Blöcke sind die realen Änderungen (aus git show 2bcc7592a),
nicht Zusammenfassungen. Sie zeigen, was in jeder Datei tatsächlich geändert
wurde.
8.1 src/databases/core/batch-module.ts
Vorher (N+1): pro Item ein eigenes UPDATE-Statement im Transaktionsloop.
let modifiedCount = 0;
await this.db.transaction(async (tx: any) => {
for (const update of updates) {
const stmt = tx
.update(table as any)
.set(utils.convertISOToDates({ ...update.data, updatedAt: now }) as any)
.where(eq((table as any)._id, update.id as string));
const result = (typeof stmt.run === "function" ? await stmt.run() : await stmt) as any;
modifiedCount += result?.changes ?? result?.rowsAffected ?? result?.count ?? 0;
}
});
return { modifiedCount };
Nachher (gruppiert, G ≤ N): einen UPDATE … WHERE _id IN (…) pro
distinktem Payload. Identische Semantik, weniger Round-Trips.
let modifiedCount = 0;
const groups = utils.groupUpdatesByPayload(updates);
await this.db.transaction(async (tx: any) => {
for (const group of groups) {
const stmt = tx
.update(table as any)
.set(
utils.convertISOToDates({
...(group.data as Record<string, unknown>),
updatedAt: now,
}) as any,
)
.where(inArray((table as any)._id, group.ids as string[]));
const result = (typeof stmt.run === "function" ? await stmt.run() : await stmt) as any;
modifiedCount += result?.changes ?? result?.rowsAffected ?? result?.count ?? group.ids.length;
}
});
return { modifiedCount };
Außerdem geändert:
- Ungenutzten
eq-Import entfernt →import { inArray } from "drizzle-orm"; - Der logisch fehlerhafte Early-Return (der bei
undefined-Payload fälschlich Erfolg{modifiedCount: 0}gemeldet hätte) wurde entfernt.
8.2 src/databases/core/relational-utils.ts
Neuer Helper nach sameBatchPayload() (endet bei L877). Gruppiert Bulk-Updates
nach identischem Payload und erzeugt je Gruppe eine Liste ids.
export function groupUpdatesByPayload<T>(
updates: Array<{ id: unknown; data?: Partial<T> | Record<string, unknown> }>,
): Array<{ ids: Array<unknown>; data?: Partial<T> | Record<string, unknown> }> {
const groups: Map<string, { ids: Array<unknown>; data?: Partial<T> | Record<string, unknown> }> =
new Map();
for (const update of updates) {
const key = canonicalPayloadKey(update.data);
let bucket = groups.get(key);
if (!bucket) {
bucket = { ids: [], data: update.data };
groups.set(key, bucket);
}
bucket.ids.push(update.id);
}
return [...groups.values()];
}
/** Canonical string for a payload: recursively sorted keys, JSON-serialized. */
function canonicalPayloadKey(data: unknown, seen = new WeakSet<object>()): string {
if (data === null || data === undefined) return "obj:{}";
switch (typeof data) {
case "boolean":
case "number":
case "string":
return `${typeof data}:${String(data)}`;
case "object": {
const obj = data as Record<string, unknown>;
if (seen.has(obj)) return "circular";
seen.add(obj);
if (Array.isArray(obj)) {
return `array:${obj.map((v) => canonicalPayloadKey(v, seen)).join(",")}`;
}
const keys = Object.keys(obj).sort();
const parts = keys.map((k) => `${k}=${canonicalPayloadKey(obj[k], seen)}`);
seen.delete(obj);
return `obj:{${parts.join(";")}}`;
}
default:
return `unknown:${String(data)}`;
}
}
Warum rekursiv kanonisch und nicht
JSON.stringify: JSON-Stringify ist key-reihenfolge-sensitiv (zwei semantisch gleiche Payloads mit anderer Key-Reihenfolge würden getrennt). Ein Shallow-Vergleich scheitert bei verschachtelten Objekten (Referenzvergleich). Die kanonische Variante sortiert Keys auf allen Ebenen → robust und deterministisch.
8.3 tests/unit/databases/groupUpdatesByPayload.test.ts (neu)
Pure-Logik-Tests des Helpers — 4/4 grün:
- Gruppiert identische Payloads korrekt.
- Trennt unterschiedliche Payloads.
- Sortiert Key-Reihenfolge invariant (Object-Key-Reihenfolge egal).
- Verschachtelte Objekte (Shallow-Referenz-Fall) → korrekt gruppiert.
8.4 tests/integration/databases/bulkfix-hetero.test.ts (neu)
Echter SQLite-Pfad-Test (bulkUpdate über db.batch) — 2/2 grün.
Beweist, dass heterogene Updates mit unterschiedlichen Payloads korrekt
persistiert werden und modifiedCount stimmt.
8.5 docs/project/competitive-security-overview.mdx (neu)
UWG-konformer, dokumentationsbasierter Vergleich der Security-/Compliance-
Controls über SveltyCMS, Payload CMS, Strapi und Directus. Keine Live-Lauf-
Behauptung; Mitbewerber-Zahlen sind als „nur öffentliche Doku” markiert.
Basiert auf der verifizierten Security-Tabelle aus competitive-comparison.mdx.
8.6 docs/project/fix_next.md (dieses Dokument)
Ehrliches Inventar: was auf next committet ist, was noch offen, plus exakte
per-Datei-Änderungsliste und die Git-Topologie-Warnung.
9. Konkrete gewünschte Änderungen, die noch OFFEN sind (kein Code)
Damit nicht wieder „angekündigt aber nicht geliefert” entsteht, hier die nicht implementierten Änderungen, die du explizit angefordert hast:
| Anforderung | Status | Was konkret fehlt |
|---|---|---|
| 2FA/MFA P0–P2 | ❌ offen | vollständige per-Datei-Spezifikation (Abschnitt 4.1): types.ts, permissions.ts, session-manager.ts, two-factor-auth.ts, collections.ts++server.ts, user-attribute-policy.ts, webauthn-service.ts |
| Zahl-Feld-Bug | ❌ offen | vorbestehend in bulkUpdate; Analyse + Fix-Vorschlag in Abschnitt 4.4 |
| Q3 Bulk-Update | ✅ 2bcc7592a |
implementiert |
| Q4 leaner Code | ⚠️ nur Vorschlag | kein Code — siehe Abschnitt 4.3 |
Ehrlichkeit: Ich habe keine 2FA/MFA-Änderung committet, weil sie nicht implementiert ist. Ich baue keine “gemachte” Änderung als erledigt ein.