feat(pages): directory publish — assets on the page's own origin (SUPER-2078)
Merged
SA
2mo ago+1132+1132+0123456789012345678901234567890123456789-58-58-01234567890123456789
Review round (Satya)
- Structure: the publish command now follows the repo guidelines —
command.tsorchestrates;utils/{collectDirectoryPublish,resolveEntryPath,registerWatch,uploadAssets,publishResult}/each own one unit with barrel + co-located tests. - One result contract:
publishResultis the single place the CLI response is shaped; data/message mirror an artifact publish (what published, where it lives, watch status), notes in a fixed order,assets/watchingalways present in the JSON. - Asset ownership hardening:
verifyPublishAssetsnow requires a file to be the caller's own upload or already attached to the target page's lineage — previously any ready org file id could be republished to a wider audience on aneveryonepage. - Implicit behaviors pinned by tests (no mocks — a real local server takes the PUTs): default response shape and note order, singular/reuse wording, one-version-deep reuse documented as a choice, lookup-failure degradation to full upload, PUT-failure error text.
- Reuse model, for the record: unchanged assets are re-referenced (same
files/<id>/originalobject across versions via the manifest) — zero bytes move, no R2 copies needed.
Links
- Ticket: SUPER-2078
- Design: Usercontent Architecture (page assets, the ticketed prefix, storage registry)
- Stacked on #7019 (files layer) → #6954 (usercontent origin); merges last.
Summary
- Directory publish:
superset pages publish ./report/—index.htmlis the page, every other file ships at its relative path and serves from the page's own origin withRange, so<video src="demo.mp4">plays and seeks. This is v1's "video inside pages".
How It Works
- CLI: collects the directory (dotfiles and
node_modulesskipped, ≤200 assets, ≤1 GiB each), hashes each asset, and reuses the previous version's file id on a match instead of re-uploading — resolved through the newpage.resolveByEntryPath, which mirrors the republish lookup (caller's own pages only, or--page <id>). New assets go create → presigned PUT → complete through the files layer. A video browsers can't reliably play (.mov,.mkv, HEVC-branded MP4) gets a warning in the output, not a refusal. - Publish: asset paths are validated before any byte moves (relative, no
.., not underversions/,files/,_superset/,~, notindex.html/thumbnail.jpg, no duplicates); every referenced file must be areadyfile in the organization;attachmentsrows land in the same transaction as the version row; the manifest gains per-versionassets: {path → {key, contentType}}. - Worker: catch-all asset routes under
/versions/<n>/[~<ticket>/]<path>and the served alias resolve through the manifest — the ticket is inherited from the document's path segment, exactly the door decision 1 chose. Assets are immutable (public/private, max-age=86400), stream with206s, and an HTML asset carries the full page CSP like the document.style-srcgains'self'so a directory's stylesheet loads. - Delete: removing a page removes its versions' attachments, and any file left with zero attachments dies with them (rows then objects).
Manual QA Checklist
Deployed-origin QA is blocked on SUPER-2080; the Worker was exercised locally (Testing).
- Publish a directory with a video; it plays and seeks in web and desktop viewers
- Republish with one changed file; the output reports the rest as unchanged
- A
.movasset publishes with a codec warning
Testing
bun run typecheck—shared,trpc,mcp,cli,content,api,webbun test— shared 883 (manifest assets), trpc page 67 (asset-path rules), cli pages 15 (directory collection, codec warning)- Worker, locally against wrangler's simulated R2 (manifests seeded with per-version assets): public asset bare + immutable with
206ranges; private asset 302s bare and serves under/versions/2/~<ticket>/…and the served alias/~<ticket>/…; HTML asset carries the page CSP; unknown asset 404; the document, thumbnail and/_superset/runtime.jsroutes are not shadowed by the catch-alls.
Design Decisions
- HTML assets get the page policy, not the media-host download policy: they are sub-documents of the same origin the page already scripts on.
- Reuse is client-side by hash (decision 11) — ids stay the identity, deletion stays "dies with its last attachment", no content-addressing.
- A directory's default title is the directory name, not "index".
Known Limitations
- Assets are read into memory for hashing and upload; a streaming upload path is a follow-up before multi-GB files matter (cap is 1 GiB).
- MCP publish stays single-file — inline payloads don't suit directories; agents use the CLI per the skill.
page.pullreturns the document only; pulling assets comes with a need for it.
Follow-ups
- Mobile page viewer (SUPER-2079) — page video plays natively in the WebView.
- Streaming hash/upload in the CLI.
Summary by cubic
Pages can now be published from a directory, so assets like video, stylesheets, and scripts serve from the page's own origin with Range support. Previously a page was one self-contained HTML file; now superset pages publish ./report/ makes index.html the document and ships every other file at its relative path, streaming with Range so <video src="demo.mp4"> plays and seeks.
Directory publish
- Asset paths are validated before any byte moves; reserved shapes, duplicates, and symbolic links are refused (a followed symlink previously let the CLI upload a file from anywhere on the machine to an org-visible page).
- Up to 200 assets at 1 GiB each; republishing reuses unchanged files by hash and reports how many stayed.
- Assets attach only if the caller uploaded them or a prior version of the page already references them.
- HTML assets carry the page CSP, and
style-src,img-src,media-src, andfont-srcgain'self'so a directory's own files load over http as well as https. - Video browsers can't reliably play (
.mov,.mkv, HEVC MP4) gets a CLI warning, not a refusal.
Lifecycle
- Deleting a page removes its versions' attachments and any file with no remaining attachments.
- Publish output mirrors an artifact publish, with assets and watching always present; MCP publish stays single-file, so agents use the CLI.
Summary by CodeRabbit
- New Features
- Publish Pages from a directory containing
index.htmland up to 200 assets. - Assets support range requests, caching, and ticket-based access.
- Unchanged assets are reused during republishing.
- Added page lookup by page ID or workspace entry path.
- Publish results report asset counts, reuse, and media warnings.
- Publish Pages from a directory containing
- Bug Fixes
- Improved asset path validation, symlink safety, and browser styling support.
- Documentation
- Added directory publishing guidance, limits, and examples.
- Tests
- Expanded coverage for manifests, assets, directory publishing, and media compatibility.