Skip to content

feat(cli): hyperframes publish — share projects via a public URL#312

Merged
miguel-heygen merged 5 commits intomainfrom
feat/cli-publish
Apr 23, 2026
Merged

feat(cli): hyperframes publish — share projects via a public URL#312
miguel-heygen merged 5 commits intomainfrom
feat/cli-publish

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Apr 17, 2026

Summary

This PR adds hyperframes publish as the OSS handoff into the persisted HyperFrames publish flow.

Instead of opening a local tunnel, the CLI now:

  1. zips the local project
  2. uploads it to the HeyGen publish backend
  3. gets back a stable hyperframes.dev project URL plus claim token
  4. prints a claimable URL for the user

Example output:

$ hyperframes publish

  Project    my-video
  Files      12
  Public     https://hyperframes.dev/p/hfp_123?claim_token=...

  Open the URL on hyperframes.dev to claim the project and continue editing.

User Flow

The intended user flow is:

  1. Run hyperframes publish from a local HyperFrames project.
  2. The CLI uploads the project as a zip to the publish API.
  3. The CLI prints a stable hyperframes.dev URL with the claim token attached.
  4. The user opens that URL in the browser.
  5. hyperframes.dev uses that URL to claim the published project and import it into the web app.
  6. The user continues editing from a normal web session.

So the CLI is only responsible for packaging, upload, and printing the URL. The browser-side claim/import flow lives in the backend and web app stack.

Routing

This PR does not expose a separate user-facing canary mode.

The CLI posts to the normal publish API host:

  • https://api2.heygen.com/v1/hyperframes/projects/publish

Backend routing behavior is handled server-side. If the default path routes through canary, it does so without a dedicated CLI flag; if that path is unavailable, traffic falls back to prod behavior on the backend side.

What Changed

File Role
packages/cli/src/commands/publish.ts Adds the hyperframes publish command, confirmation prompt, lint-before-upload behavior, and user-facing output.
packages/cli/src/utils/publishProject.ts Zips the local project, filters ignored files/directories, posts the archive to the publish API, and returns the published project metadata.
packages/cli/src/utils/publishProject.test.ts Covers archive creation and successful upload response parsing.
packages/cli/src/cli.ts Registers the new publish command.
packages/cli/src/help.ts Adds publish to root help and examples.
docs/packages/cli.mdx Documents the persisted publish flow.

Important Behavior

  • Requires index.html at the project root.
  • Ignores hidden files and common non-project directories like .git, node_modules, dist, .next, and coverage.
  • Lints the project before upload and prints findings, but does not block publish on warnings.
  • Does not keep a local process alive after upload.
  • Does not open a public tunnel.
  • Does not require HeyGen OAuth inside the CLI.

Why This Shape

This keeps the OSS CLI simple and matches the current product direction:

  • project persistence lives in HeyGen's backend
  • the public URL comes from the persisted project row
  • claiming/importing happens on hyperframes.dev
  • the CLI should not own browser auth or long-lived sharing infrastructure

Verification

In the earlier PR worktree, this flow was verified locally with the CLI build/test path and with real backend integration.

In this cleanup worktree, the narrow code/doc change was verified by inspection, but the repo-level commands are currently blocked here by missing local tool binaries and typings in the worktree environment:

  • bun run --filter @hyperframes/cli test -> vitest: command not found
  • bun run --filter @hyperframes/cli typecheck -> local dependency/type resolution failures outside this diff
  • bun run --filter @hyperframes/cli build -> tsx: command not found

Notes

This PR only covers the OSS CLI side of the flow.

The full end-to-end experience depends on the corresponding backend and hyperframes.dev changes that store published projects, return the stable URL, and support claim/import in the web app.

@mintlify
Copy link
Copy Markdown

mintlify Bot commented Apr 17, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Apr 17, 2026, 10:59 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

One-command share: `hyperframes publish` starts the preview server,
opens a public HTTPS tunnel to it, and prints a URL the user can paste
into Slack / Figma / iPhone Safari / anywhere. Token-gated, read-only
by default, explicit consent before exposure, auto-rebuilds the studio
if its assets are missing so users never see a broken tunnel.

- packages/cli/src/commands/publish.ts — new command. Consent prompt,
  mint session token, start preview through a fetch wrapper that gates
  the studio server, open the tunnel, print the URL. Ctrl-C reaps both
  children cleanly.
- packages/cli/src/utils/tunnel.ts — provider detection
  (cloudflared preferred, tuns.sh ssh fallback) and openTunnel that
  spawns the child and resolves with the first emitted public URL.
- packages/cli/src/utils/publishSecurity.ts — shared-secret token gate,
  mutation gate (read-only by default), read-only UI injection,
  interactive consent prompt.
- packages/cli/src/server/studioServer.ts — StudioAssetsMissingError
  thrown at construction if dist is missing (no lazy-500 at request
  time). Fixed the dev-fallback path: was three levels up at
  `hyperframes-oss/studio/dist`, now correctly two at
  `packages/studio/dist`.
- packages/cli/scripts/build-copy.sh — replaces the old silent
  `cp -r ../studio/dist/*`. Runs under `set -euo pipefail`, asserts
  the source index.html exists before copying, asserts the
  destination index.html exists after. A zero-file copy can no longer
  ship a broken CLI bundle.
- Root package.json — build now serializes studio first, then the
  rest in parallel. CLI dropped its nested `build:studio`; the two
  parallel `vite build` invocations were clobbering each other's
  packages/studio/dist.
- packages/cli/src/cli.ts, src/help.ts — register publish in the
  CLI, add it to the Getting Started group and root examples.
- docs/packages/cli.mdx — documents the command, flags, and the
  read-only security model.

Auto-picks the first available of:

1. cloudflared — Cloudflare's free quick tunnel. Most reliable.
   `brew install cloudflared` (Linux/Windows binaries equivalent). No
   account, no keys, no config.
2. tuns.sh — zero-install ssh -R fallback. Works on every machine
   with OpenSSH.

`--provider cloudflared | tuns | auto` forces one.

The studio server was built for localhost and everything is
unauthenticated. PUT/POST/DELETE/PATCH on /api/projects/:id/files/*
reads and overwrites the project directory. Exposing that surface
unguarded over a public URL is how you ship a "drop a keylogger in
their index.html" bug. Four composed defences:

1. Token gate. On startup, mint a 32-byte base64url token via
   crypto.randomBytes. Public URL is
   `${tunnelUrl}/?t=${token}#project/${name}`. First hit validates in
   constant time (timingSafeEqual), sets an httpOnly; Secure;
   SameSite=Lax session cookie, and 302-redirects to a clean URL so
   the token never lands in browser history or Referer. Every
   subsequent request needs the cookie. Anything else returns 404
   (deliberately, not 401/403 — do not advertise that a server is
   here). Cookie TTL: 12 h. Implemented as a fetch wrapper rather
   than a Hono middleware because the studio routes are registered
   before publish gets the app; middlewares only apply to routes
   registered after them. Wrapping fetch gates traffic at the HTTP
   boundary and sidesteps ordering entirely.
2. Mutation gate. When --allow-edit is not set, refuse writes/deletes
   on `/api/projects/:id/files/*`,
   `POST /api/projects/:id/duplicate-file`, and
   `POST /api/projects/:id/render`. Returns
   `403 { "error": "forbidden", "reason": "…re-run with --allow-edit…" }`.
   Authenticated reads still work.
3. Read-only UI enforcement. The mutation gate closes the server
   side, but the studio UI does not know about the mode. Without
   this layer, visitors would type into a Monaco editor whose saves
   silently 403. Every response carries
   `X-HF-Publish-Readonly: 1`. HTML responses from the SPA (NOT
   `/api/*`, so the composition bundle served inside the player
   iframe is untouched) get a small inline <style> + <script>
   injected before `</head>`. The style neutralises Monaco /
   CodeMirror surfaces via pointer-events: none,
   caret-color: transparent, user-select: none. The script sets
   window.__HF_PUBLISH_READONLY__ = true, renders a compact pill
   centered at the top of the viewport ("READ-ONLY · Published
   tunnel"), and monkey-patches window.fetch to short-circuit
   mutating calls client-side so the UI does not loop through
   doomed 403s.
4. Explicit consent on startup. Before the server binds, a clack
   confirm prompt spells out the exposure: token-is-a-password
   guidance, the 12 h session lifetime, and — when --allow-edit is
   set — a warning that visitors can read/write/delete project
   files. --yes / -y skips for scripts. In non-TTY environments
   without --yes, publish refuses to proceed rather than silently
   exposing.

Previously, running publish against a dev checkout without a built
studio produced "Studio not found. Rebuild with: pnpm run build" on
every request — after the tunnel was open and the URL shared. We
fix it instead: createStudioServer throws
StudioAssetsMissingError at construction, publish catches it,
spawns `bun run build` in packages/studio with a spinner, retries
createStudioServer once the build succeeds. The error surface only
fires when auto-rebuild itself fails (broken global install, etc.),
and lists every path searched plus the `bun install && bun run
build` guidance. End-to-end: deleting both packages/studio/dist
and packages/cli/dist/studio and running publish now rebuilds the
studio and opens the tunnel without user intervention.

- --port <n>               Preview port (default 3002).
- --provider <p>           cloudflared | tuns | auto.
- --allow-edit             Remote visitors can write/delete files.
                           Off by default.
- --yes / -y               Skip the consent prompt.

- No info leak on unauthenticated probes — random visitors see 404
  whether a path exists or not.
- Token never leaves the first URL — redirect strips it before any
  HTML is served, so no iframe Referer can leak it.
- Constant-time comparison for both token and cookie checks.
- No persisted state — token lives for the lifetime of the command,
  Ctrl-C nukes it.
- Read-only cannot be bypassed by a malicious client; the
  server-side 403 is the real stop.
- zero-file build:copy can no longer ship a broken CLI.

- src/utils/publishSecurity.test.ts — 23 cases: token exchange,
  cookie flow, mutation-gate allow/deny matrix,
  X-HF-Publish-Readonly header, HTML-injection + no-op on
  --allow-edit / non-HTML / /api/* paths.
- src/utils/tunnel.test.ts — 7 cases for the URL extractor
  (per-provider banner parsing, cross-provider contamination
  guard) and provider picker semantics.
- src/server/studioServer.test.ts — StudioAssetsMissingError shape
  and export for downstream catch blocks.
- Full CLI suite: 144/144 pass.
- tsc --noEmit clean, tsup build clean.
@miguel-heygen
Copy link
Copy Markdown
Collaborator Author

Checked the current head of this PR (27f42f237106c4c6693f96522bbfe39c20d527bf) and this is already addressed on the branch.

In packages/cli/src/utils/publishProject.ts, publishProjectArchive() builds a headers object and sets headers["heygen_route"] = "canary" when options.canary is true, and that same headers object is passed into the fetch() call for /v1/hyperframes/projects/publish. The CLI flag wiring is in packages/cli/src/commands/publish.ts via hyperframes publish --canary.

So the current PR branch does route publish through canary from the CLI path; no extra env-var escape hatch should be needed for this branch.

Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@miguel-heygen miguel-heygen merged commit b4e9d64 into main Apr 23, 2026
21 checks passed
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.

2 participants