Skip to content

storage: acceptSignedUpload is unreachable end to end — no mounted PUT route, and the signing secret is closed over #145

Closed

Description

@sebyx07

What

acceptSignedUpload is public API, tested, and — after the 2026-08-18 sweep — correct. It is also unreachable from a real app:

  1. No mounted PUT route. Nothing in the framework serves the upload target a grantUpload URL points at. packages/cli/src/dev-storage.ts mounts a read route only.
  2. The signing secret is closed over inside localDriver and no StorageDriver method exposes it, so a route an app writes itself cannot verify the signature either.

packages/storage/CLAUDE.md already records (1). (2) is what makes it more than a missing route: the seam an app would need is not there.

Why it matters

docs/architecture/17-uploads.md documents the round trip as a working flow. Every piece of it is real and unit-tested; the flow is not runnable. That is the shape the framework exists to prevent — the sweep found three separate defects inside this surface (the base-path mismatch, requireChecksum being unreachable, shared assets refused as cross-tenant), and all three survived precisely because no end-to-end path exercises them.

Fix direction

Decide where the upload route lives — framework-mounted like the read route, or an app-written route plus a StorageDriver seam that hands over a verifier (not the secret). Then make examples/dummy exercise the round trip, which is what would have caught all three.

Found by the multi-agent bug sweep, 2026-08-18.

Activity

  1. developerz-ai commented on Aug 18, 2026

    @developerz-ai
    Contributor

    I couldn't reliably classify or scope this report right now, so I haven't guessed or handed it off. A maintainer will review it.

    🤖 Posted by developerz.ai — the maintainer agent, not a human.

  2. developerz-ai commented on Aug 18, 2026

    @developerz-ai
    Contributor

    Thanks for this detailed report. This is a clear bug: acceptSignedUpload is implemented and tested but cannot be reached from a real application due to:

    1. No mounted PUT route in the framework
    2. The signing secret is closed over in localDriver with no public seam to access it

    This matches the pattern of the three defects found in the sweep — all survived because no end-to-end path exercises them.

    I'll label this as a bug and proceed to scope the fix.

    🤖 Posted by developerz.ai — the maintainer agent, not a human.

  3. developerz-ai commented on Aug 18, 2026

    @developerz-ai
    Contributor

    Scoped task

    Problem: acceptSignedUpload is unreachable from real apps — no PUT route exists and the signing secret is not exposed through StorageDriver.

    Acceptance criteria

    1. Framework mounts a PUT route at the upload target that grantUpload URLs point to, OR StorageDriver exposes a verifier seam for app-written routes
    2. The round trip (grantUpload -> PUT -> acceptSignedUpload) works end-to-end in examples/dummy
    3. No regression: existing acceptSignedUpload unit tests continue to pass

    Areas

    • packages/storage/driver.ts — interface needs verifier method
    • packages/storage/driver-local.ts — localDriver needs to expose verifier
    • packages/storage/upload.ts — acceptSignedUpload
    • packages/storage/grant.ts — grantUpload
    • packages/cli/src/dev-storage.ts — needs PUT route
    • examples/dummy — needs full round-trip test

    Groups

    1. Expose StorageDriver verifier seam — localDriver closes over signing secret; add interface method to get verifier
    2. Mount PUT upload route in dev server — dev-storage.ts only has read route
    3. Add end-to-end test in dummy — the full upload flow must be exercised

    Confidence: 0.8

    🤖 Posted by developerz.ai — the maintainer agent, not a human.

  4. developerz-ai commented on Aug 18, 2026

    @developerz-ai
    Contributor

    Want to submit a fix? I'll scope it and flag it for the maintainer to pick up.

    🤖 Posted by developerz.ai — the maintainer agent, not a human.

  5. sebyx07 commented on Aug 20, 2026

    @sebyx07
    ContributorAuthor

    The seam is fixed in #219. The PUT mount is not, and this issue stays accurate about that — reopening or a follow-up is warranted for the mount alone.

    Both halves of the premise verified first: no PUT anywhere in packages/ (storageRoutes returns exactly one GET; the only method: 'PUT' occurrences are grant.ts declaring the signed method and upload-client.ts issuing the fetch), and the secret closed over in localDriver's factory, reachable through no member of StorageDriver.

    The seam was the real defect, and it was exactly one member wide. StorageDriver already exposes signedUrlBase — hoisted onto the interface, by its own comment, "so the minting half and the verifying half cannot state it twice". The secret is the other half of that same pair and was never hoisted. That is why acceptSignedUpload's required secret: had no value a route could put in it, and why the PUT was never mounted.

    New StorageDriver.verifySigned?({ url, clock }) — verification, never the key. A member that returned the secret would let any holder of a driver mint a URL for any key, and would put it in every JSON.stringify(disk) a log performs. acceptSignedUpload({ url, disk, orgId, bytes, declaredContentType }) now works with no secret in the call at all. New code X_STORAGE_URL_UNVERIFIABLE (500, not 403 — nothing is wrong with the caller's URL, and reporting it as 403 sends the on-call hunting an attacker who is not there).

    A mutation gap caught during the work, worth recording: the first verifySigned test passed one clock to both the driver and the call, so "ignores the caller's clock" was not caught. A test where the disk is built with a boot clock and the caller passes a later one — a route's real shape — catches it. Without that, every expiry assertion in a suite using this seam would have passed for the wrong reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions