feat(cli): hyperframes publish — share projects via a public URL#312
feat(cli): hyperframes publish — share projects via a public URL#312miguel-heygen merged 5 commits intomainfrom
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
e61f16b to
66df6fa
Compare
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.
66df6fa to
e40d393
Compare
|
Checked the current head of this PR ( In 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. |
Summary
This PR adds
hyperframes publishas the OSS handoff into the persisted HyperFrames publish flow.Instead of opening a local tunnel, the CLI now:
hyperframes.devproject URL plus claim tokenExample output:
User Flow
The intended user flow is:
hyperframes publishfrom a local HyperFrames project.hyperframes.devURL with the claim token attached.hyperframes.devuses that URL to claim the published project and import it into the web app.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/publishBackend 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
packages/cli/src/commands/publish.tshyperframes publishcommand, confirmation prompt, lint-before-upload behavior, and user-facing output.packages/cli/src/utils/publishProject.tspackages/cli/src/utils/publishProject.test.tspackages/cli/src/cli.tspublishcommand.packages/cli/src/help.tspublishto root help and examples.docs/packages/cli.mdxImportant Behavior
index.htmlat the project root..git,node_modules,dist,.next, andcoverage.Why This Shape
This keeps the OSS CLI simple and matches the current product direction:
hyperframes.devVerification
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 foundbun run --filter @hyperframes/cli typecheck-> local dependency/type resolution failures outside this diffbun run --filter @hyperframes/cli build->tsx: command not foundNotes
This PR only covers the OSS CLI side of the flow.
The full end-to-end experience depends on the corresponding backend and
hyperframes.devchanges that store published projects, return the stable URL, and support claim/import in the web app.