Fix attachment download error mapping and Content-Type resolution; add GC endpoint - #68
Merged
Merged
Conversation
…ent-Type, add GC endpoint Fixes #48: GET /attachments/:fileId no longer collapses every authenticated denial to 401 or masks unexpected errors as 404. Only an anonymous StackPermissionError becomes 401 now; a verified-but-denied requester gets 403, StackNotFoundError still maps to 404, and anything else propagates to the global error handler and is logged. Missing and inaccessible files stay indistinguishable for non-owners, per the anti-oracle rule core already enforces. Fixes #37: the download route now resolves Content-Type from all three spec'd sources instead of two — ?contentType, then ?filename extension inference, then (newly) the fileId's first-recorded _attachment@1 metadata record — via core's resolveAttachmentDownloadContentType(), replacing the local deny-list/extension-table with core's safe-list. Content-Disposition's filename now prefers the requester's own record, falling back to the first-recorded one. Fixes #38: adds owner-only POST /attachments/gc, wired to ScopedStack.collectAttachmentGarbage(), accepting optional { graceMs, dryRun } and returning { deleted, reclaimedBytes }. Closes #46 (already landed alongside #52's atomic-upload fix; this commit doesn't touch upload) and #52 (no action; already closed). Also pins @haverstack/blob-adapter-disk to ^0.8.0 via a pnpm override. The resolved 0.7.0 depended on @haverstack/core@^0.9.0 while the rest of the tree is on 0.10.0, so DiskBlobAdapter's StackNotFoundError was a different class than the one @haverstack/wire-types checks for — instanceof failed silently and a missing attachment 500'd instead of 404ing. The old blanket catch-all in this route masked it; removing that catch-all for #48 exposed it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GdVbbu2KmZkfcvRmY4BRq
adapter-local@0.9.0 now declares blob-adapter-disk@^0.8.0 directly (the range gap fixed upstream), so the pnpm.overrides workaround from the previous commit is no longer needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GdVbbu2KmZkfcvRmY4BRq
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.
Summary
Addresses the five attachment-related issues (#52, #37, #46, #48, #38):
GET /attachments/:fileIdno longer collapses every denial to 401 or masks unexpected errors as a bare 404. Only an anonymousStackPermissionErrorbecomes 401; a verified-but-denied requester now gets 403;StackNotFoundErrorstill maps to 404; anything else propagates to the global error handler (500, logged). Missing and inaccessible files stay indistinguishable for non-owners — core already guarantees this (ScopedStack.getAttachmentthrows the same error either way), the route just needed to stop overriding it.Content-Typefrom all three spec'd sources instead of two:?contentType, then?filenameextension inference, then (newly implemented) the fileId's first-recorded_attachment@1metadata record. Wired through core'sresolveAttachmentDownloadContentType()(@haverstack/core/wire), replacing the local deny-list/extension-table with core's safe-list.Content-Disposition's filename now prefers the requester's own record, falling back to the first-recorded one.POST /attachments/gc, wired toScopedStack.collectAttachmentGarbage(), accepting optional{ graceMs, dryRun }and returning{ deleted, reclaimedBytes }.Incidental dependency fix
Bumps
@haverstack/adapter-localto0.9.0. While fixing #48 I removed a blanketcatchthat had been silently absorbing every non-permission error as a hardcoded 404 — with it gone, a genuinely-missing attachment started 500ing instead of 404ing. Root cause: the resolved@haverstack/blob-adapter-disk@0.7.0(transitive viaadapter-local@0.8.0) still depended on@haverstack/core@^0.9.0while the rest of the tree was on0.10.0, soDiskBlobAdapter'sStackNotFoundErrorwas a different class than the one@haverstack/wire-types'serializeErrorchecks for, andinstanceofsilently failed.This went through two states: an earlier commit on this branch pinned
blob-adapter-diskto^0.8.0via apnpm.overridesworkaround, sinceadapter-local@0.8.0's own declared range (^0.7.0) excluded the fixed version.adapter-local@0.9.0has since been published upstream with the correctedblob-adapter-disk: ^0.8.0dependency, so this PR now just bumps the real dependency and the override is gone.Test plan
pnpm typecheckcleanpnpm lintcleanpnpm format:checkcleanpnpm test— 238/238 passing, including new coverage:docs/api.mdupdated with the GC endpoint and the download resolution/anti-oracle behavior🤖 Generated with Claude Code
https://claude.ai/code/session_012GdVbbu2KmZkfcvRmY4BRq