Repository navigation
feat(uploads): direct-to-S3 channel/DM attachments; retire processor upload router - #1513
Conversation
…upload router Migrate channel and DM attachment uploads off the processor's multipart /api/upload/single onto the same presign -> PUT(Tigris) -> complete flow page files already use (#1460). Bytes now go browser -> S3 directly; the only server-side touch is the processor re-hashing the stored object before the file row is linked. Hard cutover (the attachment direct-to-S3 surface was unreleased) -- no dual path. Architecture (pure decisions, effects at the edges): - packages/lib attachment-upload-core.ts: pure validation, file-row/result builders, slotTargetMatches; now the single source of AttachmentTarget / FileRecordInput (re-exported from attachment-upload.ts, which is reduced to createAttachmentUploadServiceToken). - apps/web lib/upload: attachment-direct (presign/complete/cancel orchestration), attachment-verify-effect (web -> processor verify), attachment-client (client 3-step), route helpers/handlers; six thin routes under {channels/[pageId]|messages/[conversationId]}/upload/{presign,complete,cancel}. - useAttachmentUpload hook does presign -> PUT -> complete per file; same public FileAttachment API and uploadUrl base (sub-routes derived). - UploadSlotMetadata gains an optional attachmentTarget binding so a presign jobId can't be replayed against a different conversation/page. Integrity (preserved, not regressed): new processor POST /api/verify (verify.ts + pure verify-core.ts), mounted with files:write scope and WITHOUT requirePageBinding so conversation tokens work. It re-hashes the stored S3 object (zero-trust; deletes poisoned objects on mismatch) and returns the Magika-detected MIME, which /complete persists instead of the client-declared type. New contentStore.headOriginalSize HEAD-probes size before download (rejects > 1 GiB with 413) and re-throws genuine infra errors -- unlike getOriginal, which swallows them -- so a transient S3 outage returns a retryable 503 instead of masquerading as a definitive not-found. Contract: 200 = definitive verdict (ok / mismatch / absent), 413 = too large, 503 = retryable infra. Hard cutover: delete the entire processor upload router (/single + /multiple, upload-multer-config, mount), the legacy web multipart POST routes, and processAttachmentUploads/uploadOneFile, plus their tests. The processor no longer receives upload bytes at all -- it only reads from S3 (serve, pull-verify, verify). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 13 minutes and 3 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR refactors attachment upload from a unified multipart flow to a direct-to-S3 architecture with processor-side zero-trust byte verification. The processor gains a new ChangesDirect-to-S3 Upload Refactor
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Web as Web API
participant Processor as Processor
participant S3 as S3
Client->>Web: POST /presign (hash, type, size)
Web->>Web: resolve target, check quota
Web->>S3: check object exists
S3-->>Web: exists or not
alt object exists
Web-->>Client: {alreadyExists: true}
else object missing
Web->>S3: get presigned PUT URL
S3-->>Web: presigned URL
Web-->>Client: {status, jobId, uploadUrl}
end
Client->>S3: PUT file to uploadUrl
S3-->>Client: 200
Client->>Web: POST /complete (jobId)
Web->>Web: validate slot target
Web->>Processor: POST /verify (hash)
Processor->>S3: HEAD + GET object
S3-->>Processor: bytes
Processor->>Processor: verify hash, detect MIME
Processor-->>Web: {ok, detectedMime, size}
Web->>Web: persist file, link to target
Web-->>Client: {success, file}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a93157630
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ative size, fix tests Address PR #1513 review + CI: - Codex P2: wrap the processor verify call in completeAttachment in try/catch so a thrown error (network / AbortSignal.timeout) still releases the reserved upload slot and decrements the active-upload count, returning a retryable 503 instead of leaking the slot until the semaphore's stale sweep. - Codex P2: persist and charge the verifier's authoritative byte length (verify.size) instead of the client-declared presign size. The presigned PUT enforces ContentLength for fresh uploads, but the dedup path skips the PUT, so a client could otherwise declare a smaller size against a pre-existing object and forge the file row / under-report storage. - Fix Unit Tests + Security Test Suite: rewrite useAttachmentUpload.test.ts for the new 3-step flow (mock uploadAttachment); add the six direct-to-S3 attachment routes to the security-audit-coverage allowlist (audit is emitted by the shared attachment-direct orchestrator + route resolvers, not inline in the thin routes). - Add orchestrator tests for the verify-throw release path and verified-size persistence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loadAttachment never throw Proactive review-round hardening for the direct-to-S3 attachment flow: - Security (content-type spoof): the verify endpoint now applies the same isAllowedContentType denylist the page-file ingest path enforces (s3-pull-adapter), rejecting + deleting browser-executable markup, scripts, and native executables based on the ACTUAL bytes. presign already blocks these when *declared*; this closes the spoof where a client declares image/png but uploads SVG/HTML/EXE bytes — which (now that we persist the detected MIME) would otherwise be stored and served inline. New 'blocked_type' verify verdict (200 ok:false, object deleted) → web maps to 415 "file type not allowed". - Robustness (client): uploadAttachment is now genuinely never-throwing, honoring its contract. computeContentHash and the presign-body JSON parse are guarded, so a WebCrypto/parse failure returns a result instead of throwing out of the per-file loop and aborting the whole batch (a regression vs the old per-file multipart flow). Every post-presign failure (PUT throw, complete non-ok, malformed complete body, any throw) cancels the reserved slot, and the real error cause (e.g. Tigris "Upload failed with status 403") is surfaced instead of a blanket message. Tests: verify-core blocked_type mapping; verify route deletes + returns blocked_type on a disallowed detected label; web interpretVerifyResponse 415 for blocked_type; client never-throws on hash/parse failure and surfaces the real PUT error. Processor lint + typecheck clean; web next build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proactive review round (self-review + multi-angle critique)Ran a high-recall review across correctness / removed-behavior / cross-file / cleanup angles. Fixed (commit
Assessed and intentionally deferred (with rationale):
All other paths verified clean: semaphore +1/−1 accounting balanced across success/dedup/verify-fail/throw/cancel; deleted-service behavior (quota, audit, integrity, sanitized filename, email-verify, participant checks) faithfully re-established; hook public contract unchanged. |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/processor/src/api/__tests__/verify.test.ts`:
- Around line 122-129: The test in 'deletes the object and returns blocked_type
when the detected type is disallowed' still expects HTTP 200 but the new verify
contract returns 415 for blocked_type; update the assertion in this test to
expect response.status toBe(415) (and keep the existing body assertions and
mockDeleteOriginal check) so the test matches the hardened verify contract;
locate the test by the it(...) description and the mocks mockDetectContentType
and mockIsAllowedContentType to change the status expectation.
In `@apps/web/src/hooks/__tests__/useAttachmentUpload.test.ts`:
- Around line 28-37: The test helper reuses the same attachment object causing
tests to be order-dependent; update the ok helper (and the other occurrence at
lines ~131-132) to return a fresh copy instead of the shared attachment
reference by creating and returning a shallow-cloned/new object each call
(preserve fields id, originalName, size, mimeType, contentHash and allow an
optional parameter to override values), so any mutations (e.g., adding
instanceId) do not leak between tests; keep the ok and fail return shapes the
same.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: eda218f2-31d4-4d14-968d-c3f8efe212ad
📒 Files selected for processing (13)
apps/processor/src/api/__tests__/verify-core.test.tsapps/processor/src/api/__tests__/verify.test.tsapps/processor/src/api/verify-core.tsapps/processor/src/api/verify.tsapps/web/src/app/api/__tests__/security-audit-coverage.test.tsapps/web/src/hooks/__tests__/useAttachmentUpload.test.tsapps/web/src/lib/upload/__tests__/attachment-client.test.tsapps/web/src/lib/upload/__tests__/attachment-direct.test.tsapps/web/src/lib/upload/__tests__/attachment-verify-effect.test.tsapps/web/src/lib/upload/attachment-client.tsapps/web/src/lib/upload/attachment-direct.tsapps/web/src/lib/upload/attachment-verify-effect.tspackages/lib/package.json
🚧 Files skipped from review as they are similar to previous changes (10)
- apps/processor/src/api/tests/verify-core.test.ts
- apps/web/src/lib/upload/tests/attachment-client.test.ts
- apps/processor/src/api/verify-core.ts
- apps/web/src/lib/upload/attachment-verify-effect.ts
- packages/lib/package.json
- apps/web/src/lib/upload/attachment-client.ts
- apps/web/src/lib/upload/tests/attachment-verify-effect.test.ts
- apps/web/src/lib/upload/tests/attachment-direct.test.ts
- apps/processor/src/api/verify.ts
- apps/web/src/lib/upload/attachment-direct.ts
# Conflicts: # apps/processor/src/api/__tests__/upload-multer-config.test.ts # apps/processor/src/api/upload-multer-config.ts
…bbit nit)
ok() now returns { ...a } per call so tests never share an attachment reference.
The hook never mutated it (spreads into a fresh FileAttachment), so this is
defensive hygiene, not a bug fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What & why
Migrates channel and DM attachment uploads off the processor's multipart
POST /api/upload/singleonto the same presign → PUT(Tigris) → complete flow page files already use (#1460). Bytes now go browser → S3 directly; the only server-side touch is the processor re-hashing the stored object before the file row is linked.This started from the merge note on #1460 asking whether the sibling
/api/upload/multipleshould be moved too. Ground-truth audit:/multiplewas dead code (zero callers), and/singlewas the last thing keeping the processor in the upload-bytes business — its only caller was the channel/DM pipeline. So the answer became the full migration + cleanup.Hard cutover — the attachment direct-to-S3 surface was unreleased, so there is no dual path.
Architecture (pure decisions, effects at the edges)
packages/libattachment-upload-core.ts— pure validation, file-row/result builders,slotTargetMatches. Now the single source ofAttachmentTarget/FileRecordInput(re-exported fromattachment-upload.ts, which is reduced tocreateAttachmentUploadServiceToken).apps/weblib/upload/—attachment-direct(presign/complete/cancel orchestration),attachment-verify-effect(web→processor verify),attachment-client(client 3-step),attachment-route-helpers/-handlers. Six thin routes under{channels/[pageId]|messages/[conversationId]}/upload/{presign,complete,cancel}.useAttachmentUploadhook does presign → PUT → complete per file — same publicFileAttachmentAPI anduploadUrlbase (sub-routes derived), so consumers are untouched.UploadSlotMetadatagains an optionalattachmentTargetbinding, so a presignjobIdcan't be replayed against a different conversation/page.Integrity (preserved, not regressed)
New processor
POST /api/verify(verify.ts+ pureverify-core.ts), mounted withfiles:writescope and withoutrequirePageBindingso conversation tokens work. It:/completepersists instead of the client-declared type.New
contentStore.headOriginalSizeHEAD-probes size before download (rejects > 1 GiB with 413) and re-throws genuine infra errors — unlikegetOriginal, which swallows them — so a transient S3 outage returns a retryable 503 instead of masquerading as a definitive not-found.Verify contract:
200= definitive verdict (ok /hash_mismatch/object_not_found),413= too large,503= retryable infra. Only 5xx is retryable.Cleanup (the satisfying part)
Deleted the entire processor upload router (
/single+/multiple,upload-multer-config, server mount), the legacy web multipart POST routes, andprocessAttachmentUploads/uploadOneFile— plus their tests. The processor no longer receives upload bytes at all — it only reads from S3 (serve, pull-verify, verify).Testing
TDD throughout (pure logic isolated from effects). New suites: core (15), processor verify route + pure core (21+10),
headOriginalSize(5), web orchestrator (15), verify-effect (7), route helpers (8), client (9).bun run typecheck— 13/13 turbo tasksnext build— typecheck + ESLint + compile cleanChannelInput) unaffected🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Refactoring
Review round (convergence)
Codex P2s — both fixed (commit
f136cc1) + regression tests + threads resolved:verifyAttachmentBytescall incompleteAttachmentis now wrapped in try/catch — a thrown error (network /AbortSignal.timeout) releases the reserved semaphore slot + active-upload count and returns a retryable 503, instead of leaking the slot until the stale-slot sweep.completenow persists and charges the verifier's authoritative byte length (verify.size, what the processor actually re-read from S3) rather than the client-declared presign size — closing the gap where thealreadyExists(dedup) path skips the size-enforcing PUT.CI fixes (commit
f136cc1): rewroteuseAttachmentUpload.test.tsfor the 3-step flow (mockuploadAttachment); added the six attachment routes to thesecurity-audit-coverageallowlist (audit is emitted by the shared orchestrator/resolver, not inline in the thin routes).CodeQL alerts on
verify.ts— assessed, threads resolved with justification:/api/verifyis service-to-service (web → processor, service-token auth) and already rate-limited upstream by the per-user upload semaphore (tier concurrency cap) + storage quota; the processor has no per-endpoint limiter on any route by design.js/missing-rate-limitingis a pre-existing accepted pattern (open alerts already onmaster); this PR net-removes several by deleting the upload router.contentHashis the verification target, anddeleteOriginalfires only on a hash mismatch of a content-addressed key the caller themselves populated (presign returnsalreadyExistsfor valid existing objects), so there's no cross-tenant impact.Synced with
master(merge3426c6cb) — no conflicts; rebuilt + re-validated the combined tree.Validation (merged tree):
next buildclean (typecheck + ESLint + compile); lib attachment suites (24), processor verify/content-store/server (182), web upload + audit suites (69) all green; Unit Tests green in CI on the prior commit.