feat(intake): upload a request and enqueue processing atomically - #32
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ion access control Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…LS with integration proofs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…for #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…boss queues Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…nd download routes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…-only audit, composite FK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
- sign-up requires the invitation id (link) plus the invited e-mail – no takeover by address alone - one company per user (unique index), deterministic membership lookup, actor from membership - organization plugin accepts only admin/clerk roles - configurable client-IP source for the auth rate limit; local secret refused in production - invite page: zod input, 404 for clerks, shows the invitation link; signup needs the link - tests: wrong/missing invitation id, foreign set-active/list-members, last admin, roles - docs: operations (rate limit/proxy, recovery), data model, exceptions register (admin plugin) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…e rejection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
… docs for #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…secret The image sets NODE_ENV=production, so the previous check blocked the local compose stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
This was referenced Sep 23, 2026
…heck, lock, streaming Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
- pg-boss client and queue install move to src/db (job-queue.ts); least-privilege grants; depcruise rule - upload: Content-Length required (411), request cap (413), OOXML structure check (macros, foreign ZIPs, zip bombs) - duplicate detection serialised per company (advisory lock); orphaned objects logged; streaming download - composite same-company FKs declared in the Drizzle schema (migration 0005, fresh-DB tested) - tests: job row proven inside the rolled-back transaction, audit rolled back, 411/413/too many files - test helper: random IPv6 per call – no rate-limit bleed across test files - docs: api.md limits, exceptions register (upload rate limit, .msg check) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Owner
Author
Frischer Review + Security-Review (unabhängiger Subagent) – Befunde und AuflösungReview nach
Beim Nachtest gefunden und behoben: der Test-Helper vergab Fake-IPs pro Testdatei neu → Rate-Limit-Übertrag zwischen Dateien (429). Jetzt zufällige IPv6-/64-Präfixe pro Aufruf. Hinweis Hook: Verdict des Reviewers: nach Behebung mergebar, menschliche Freigabe nötig (Security, Migration, öffentliche API). Generated by Claude Code |
…-db-connection rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warum
Fixes #5 · Teil von Epic #2 · gestapelt auf #31 (Basis
claude/feat-identity-tenancy-4).Arbeitsstand
pnpm verifylokal grün; Migrationen auf frischer Datenbank getestet.Was ist passiert (Klartext)
Sachbearbeiter:innen können jetzt unter
/requestseine Anfrage hochladen – eine E-Mail oder lose Dateien. Das System prüft jede Datei streng: erlaubte Endung und passender Inhalt (eine umbenannte Programmdatei oder eine Excel-Datei mit Makros wird abgelehnt), Größe und Anzahl sind begrenzt. Die Originale landen in einem privaten Speicher; herunterladen kann sie nur, wer zur selben Firma gehört.Anfrage, Dokumente, Protokolleintrag und der Verarbeitungsauftrag entstehen in einem einzigen Schritt: Geht irgendetwas schief, gibt es nichts davon – auch keine halb angelegte Anfrage und keine verwaiste Datei. Doppelte Anfragen (gleiche E-Mail-Kennung oder genau dieselben Dateien) werden erkannt, markiert und mit dem Original verknüpft, aber nie verworfen. Das Protokoll kann von der Anwendung nur ergänzt, nie geändert oder gelöscht werden.
Plan-Pflicht (SYSTEM.md §4)
Impact Manifest
intake(Validierung inkl. OOXML-Struktur, Fingerprint, Mail-Header,submitUpload),documents,storage(put/get/stream/delete),audit(recordAudit),jobs(Queue-Definitionen, transaktionales Enqueue),requests(Intake-Spalten, Duplikatsuche mit Lock),db(Migrationen 0003–0005,job-queue.ts,job-queue-client.ts),app(Routen, Seiten/requests,/requests/:id),config(Upload-Limits).POST /api/requests(multipartfiles),GET /api/documents/:id(Vertrag indocs/technical/api.md); Tabellenapp.documents,app.audit_events(FORCE RLS; Audit append-only per Grants);app.requests+ Intake-Spalten; Composite-FKs(…, company_id); Schemapgboss(Deploy-Schritt alsapp_owner,app_rwmit engen Rechten).send(…, { db: fromDrizzle(tx, sql) })schreibt den Job über die übergebene Transaktion (Test zählt ihn darin); Policyexclusive+singletonKey= höchstens ein wartender/aktiver Job pro Anfrage; Installation alsapp_owner, Senden alsapp_rwfunktioniert mit den engen Grants.nosniff; zwei akzeptierte Restrisiken im Ausnahmenregister (siehe feat(intake): upload a request and enqueue processing atomically #5); Migrationen additiv; Rollback per Revert.Akzeptanzkriterien → Nachweis
files.test.ts,ooxml.test.ts(Unit);upload-routes.test.ts(422 mit Meldung, 411, 413, zu viele Dateien){companyId}/{requestId}/{documentId}privat; Download nur authentifiziert mit Tenant-Prüfungintake.test.ts(Schlüssel, Objekt vorhanden);upload-routes.test.ts(eigene Firma 200, fremde 404, anonym 401)intake.test.ts: Job-Zeile innerhalb der Transaktion sichtbar (1), danach 0; Anfrage, Dokumente, Audit weg; gespeicherte Objekte gelöschtfingerprint.test.ts(Unit);intake.test.ts(Message-ID, Datei-Set in anderer Reihenfolge, nur innerhalb der Firma)..msg-Message-ID: bekannte Grenze (api.md, #23)intake.test.ts(request.uploadedmit Akteur); append-only per Grants getestetGeändert
src/features/intake/(files.ts,ooxml.ts,fingerprint.ts,mail-headers.ts,submit.ts, Tests,zip-fixture.ts),src/features/documents/,src/features/audit/,src/features/jobs/,src/features/requests/repository.ts,src/features/storage/s3-blob-store.ts.src/db/schema/app.ts, Migrationen0003_intake.sql,0004_intake_force_rls_audit.sql,0005_intake_same_company_fks.sql;src/db/job-queue.ts(Enqueue in Transaktion),src/db/job-queue-client.ts(Pool-Fabrik + Installation mit engen Grants).src/app/api/requests/route.ts,src/app/api/documents/[id]/route.ts,src/app/requests/*,src/app/_server/runtime.ts, Startseite;src/setup.tsinstalliert die Queues..dependency-cruiser.cjs: Regelnpg-boss-client-only-in-db, Pool-Fabrik unterno-db-connection-in-features(+ Fixture).src/config/env.ts,.env.example:UPLOAD_MAX_FILE_BYTES,UPLOAD_MAX_FILES,UPLOAD_MAX_REQUEST_BYTES.tests/integration/{intake,upload-routes}.test.ts; Helper mit zufälligen IPv6-Adressen (kein Rate-Limit-Übertrag).data-model.md,api.md, Architekturkarte (Status, zwei Ausnahmen), CHANGELOG.Nachweis (SYSTEM.md §11)
verify:changed: grünverify: grün – lokal: lint, typecheck, 48 Unit + 44 Integrationstests (PostgreSQL 17, SeaweedFS 4.47, pg-boss), depcruise 0 Verstöße, build, audit 0 high; alle Migrationen zusätzlich auf frischer Datenbank (requestflow_fresh) angewandt, 44/44 grün. CIcheck+compose-smoke: siehe Checks dieses PRs.verify:full/ E2E-Spec: nicht betroffen/requestsim Build.Doku-Entscheidung (genau eine)
docs/technical/data-model.md,docs/technical/api.mddocs/technical/architecture.md(Status, Ausnahmenregister)[Unreleased](sichtbares Feature oder Verhalten – im selben PR, nie „später")Entferntes oder Umbenanntes: nichts entfernt
Dateigrößen und neue Bausteine (SYSTEM.md §7)
Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):
Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen
Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:
src/; gefunden: nichts – Moduleintake,documents,audit,jobssind Skelette aus chore(app): TS app skeleton, docker compose and verify commands #3;storagewird erweitert statt dupliziertSubagent-Einsätze
Risiken / offene Punkte
.msgnur per Signatur geprüft.drizzle-kit generatelief im separaten Worktree mitFLUORY_NO_CHECKPOINT=1, weil der Checkpoint-Guard das Session-Verzeichnis prüft (dort lag ein anderer Branch mit unfertigen Dateien); der Worktree war committet.🤖 Generated with Claude Code
https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1