Skip to content

fix(uploads): underscore-led keys, bounded bucket names, dedup only on live objects - #1757

Merged
pyramation merged 2 commits into
mainfrom
feat/static-deploy-upload-fixes
Aug 18, 2026
Merged

fix(uploads): underscore-led keys, bounded bucket names, dedup only on live objects#1757
pyramation merged 2 commits into
mainfrom
feat/static-deploy-upload-fixes

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

Three independent defects that each break static-site deploys, reported as constructive-planning #1754, #1755 and #1756.

#1754 — a leading underscore was an invalid custom key. Every Next.js export dies on its first _next/static/** file. Validation moves out of plugin.ts into custom-key.ts (exported, so callers can validate before requesting a URL) and admits the underscore; traversal, leading slash, NUL and the 1024-char bound are unchanged.

-/^[a-zA-Z0-9][a-zA-Z0-9_.\-/]*$/
+/^[a-zA-Z0-9_][a-zA-Z0-9_.\-/]*$/

#1755 — physical bucket names were unbounded and could be illegal. The name was ${prefix}-${bucketKey}-${databaseId}: a 36-char UUID plus any prefix overruns S3's 63-char limit, and neither component was case- or charset-constrained. Each component is now sanitized to [a-z0-9-], truncated to a readable budget, and closed with a 12-hex digest of the full logical identity, so two keys that truncate to the same head still get distinct names:

mint(prefix, databaseId, bucketKey)
  = sanitize(prefix)[..20] + "-" + sanitize(bucketKey)[..28] + "-" + sha256(prefix/databaseId/bucketKey)[..12]

No migration: platform_buckets.physical_name is authoritative once stored, and both the lazy and eager lanes only call the resolver when it is NULL — already-provisioned buckets keep their names.

#1756 — dedup treated any row matching the content hash as a stored object. A row whose upload was only requested, or was rejected/expired, carries no bytes, but both the direct-presigned and managed paths answered deduplicated: true and handed back its key — a deploy that silently references nothing. A row is live only when the files table carries the confirm-upload lifecycle at all and its status is uploaded/processed:

isLiveFileRow(cfg, row) = !cfg.hasConfirmUpload || ['uploaded', 'processed'].includes(row.status)

hasConfirmUpload comes from storage_modules.has_confirm_upload; when the lifecycle is absent there is nothing to read, so every row stays live as before. A stale row is deleted rather than reused: content-addressed keys equal the hash, (bucket_id, key) is unique so a replacement cannot be inserted alongside it, and reusing a rejected row would not re-run the lifecycle from the start.

Bucket-name expectations in the settings and server-test suites move to the digest form; upload.integration.test.ts continues to assert a stored physical_name is honored verbatim.

Note: s3-signer.integration.test.ts fails locally only because no MinIO is running on this box.

Link to Devin session: https://app.devin.ai/sessions/924488078ac34f83bc9c1af8e192af0f
Requested by: @pyramation

…nly live objects

Three independent static-deploy upload defects:

- Custom keys rejected a leading underscore, so every Next.js export failed at
  its first _next/static/** file. Validation moves to a shared module and admits
  the underscore while keeping traversal, leading-slash, NUL and length guards.
- Physical S3 bucket names were prefix-key-databaseId, which overran the 63-char
  limit and could carry illegal characters. Names are now sanitized, truncated
  to a readable head and suffixed with a digest of the full logical identity.
  Existing buckets are untouched: physical_name is authoritative once stored.
- Deduplication treated any row matching a content hash as a live object, so an
  upload that was only requested, or was rejected or expired, could be reported
  as already stored while no bytes existed. Dedup now requires a confirmed
  status; a stale row is deleted and the upload restarted.
@pyramation pyramation self-assigned this Aug 18, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@blacksmith-sh

This comment has been minimized.

@pyramation
pyramation merged commit b0b40a5 into main Aug 18, 2026
20 checks passed
@pyramation
pyramation deleted the feat/static-deploy-upload-fixes branch August 18, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant