fix(cli): publish projects through staged uploads#491
Conversation
jrusso1020
left a comment
There was a problem hiding this comment.
CLI staged-upload flow looks good. Verified end-to-end against the companion EF PR #35557 running on local_dev: the CLI hit /publish/upload, PUT 56,916,833 bytes directly to the presigned S3 URL with the right upload_headers (content-type + x-amz-server-side-encryption: AES256), then /publish/complete returned https://hyperframes.dev/p/hfp_f9e6bf08c522?claim_token=…. No Cloudflare challenge — that's the win.
The defensive bits in getUploadHeaders() are nice: it parses X-Amz-SignedHeaders from the presigned URL and ensures every signed header is actually sent on the PUT, plus the SSE fallback for older staged responses that don't return explicit upload_headers. Falling back to legacy multipart on 404/405 from /publish/upload keeps the CLI compatible with envs that don't have the new endpoints deployed yet.
Local checks:
bun run --filter @hyperframes/cli test -- src/utils/publishProject.test.ts→ 4/4 pass- E2E publish against local heygen_server: staged path, 200/200/200, project persisted, public URL resolves through
/public
CI: Lint / Format / Typecheck / Build / Test / Test: runtime contract / Smoke / CodeQL / Perf:* / player-perf — all ✅. Tests on windows-latest and regression-shards still in flight but the changed surface is CLI utils only.
— Rames Jusso
Problem
hyperframes publishuploads the project ZIP through the API edge. Valid projects with binary/high-entropy media can be challenged before they reach Flask, so publish fails even when the archive is valid.While validating the staged backend flow on devbox, the client also exposed a second contract issue: S3 PUTs must send every header signed into the presigned URL. The backend signs
x-amz-server-side-encryption, so a client that only sendscontent-typegetsSignatureDoesNotMatchfrom S3.A backend review follow-up now signs the archive byte length too. The CLI already sends
content_lengthwhen requesting the upload; this PR also honorscontent-lengthwhen it appears inX-Amz-SignedHeaders, so the PUT request does not rely on runtime-specific implicit header behavior.Reproduction
/Users/miguel07code/Downloads/apple-presentation.hyperframes publish --yes /Users/miguel07code/Downloads/apple-presentationagainst the existing direct multipart endpoint.cf-mitigated: challengeand Cloudflare HTML.x-amz-server-side-encryption, but the CLI PUT only sentcontent-type; S3 returnedSignatureDoesNotMatch.Root Cause
The CLI was sending binary ZIP bytes through the API edge instead of uploading directly to object storage. The staged path also needs to treat presigned upload headers as part of the API contract: every signed header must be sent with the S3 PUT.
What This Fixes
/v1/hyperframes/projects/publish/upload.content_length.x-amz-server-side-encryption: AES256when that header is signed but missing fromupload_headerscontent-length: <archive byte length>whencontent-lengthis signed/v1/hyperframes/projects/publish/complete.Verification
Local checks
bunx oxfmt --check packages/cli/src/utils/publishProject.ts packages/cli/src/utils/publishProject.test.tsbunx oxlint packages/cli/src/utils/publishProject.ts packages/cli/src/utils/publishProject.test.tsbun run --filter @hyperframes/cli test -- src/utils/publishProject.test.ts-> 4 tests passbun run --filter @hyperframes/cli typecheckgit diff --checklint,format,typecheck,commitlintDevbox integration
Before the later
content-lengthreview follow-up, I verified the staged upload flow on devbox:127.0.0.1:8029, leaving the existingbyos/local_dev stack on8019untouched.POST /v1/hyperframes/projects/publish/uploadreturnsupload_headerswithcontent-typeandx-amz-server-side-encryption.HYPERFRAMES_PUBLISHED_PROJECTS_API_URL=http://127.0.0.1:18029 bun run --filter @hyperframes/cli dev publish --yes /Users/miguel07code/Downloads/apple-presentationPOST /v1/hyperframes/projects/publish/upload 200 OKforapple-presentation.zipwithcontent_length: 56888466POST /v1/hyperframes/projects/publish/complete 200 OKhfp_8b56d135d0cfand a claim URL.The later signed
content-lengthclient hardening was validated with local checks only.Notes