feat(cli): publish HyperFrames projects to persisted hyperframes.dev URLs#406
Closed
miguel-heygen wants to merge 3 commits intomainfrom
Closed
feat(cli): publish HyperFrames projects to persisted hyperframes.dev URLs#406miguel-heygen wants to merge 3 commits intomainfrom
miguel-heygen wants to merge 3 commits intomainfrom
Conversation
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.
# What changed
- 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.
# Tunnel
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.
# Security hardening
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.
# Auto-repair for missing studio assets
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.
# Flags
- --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.
# Properties worth calling out
- 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.
# Tests
- 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.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Collaborator
Author
|
Superseded by #312, which contains the current persisted publish flow and canary routing work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hyperframes publishflow with persisted project uploadhyperframes.devURLVerification
packages/cli/node_modules/.bin/vitest run packages/cli/src/utils/publishProject.test.tsNotes