feat(pages): directory publish — assets on the page's own origin (SUPER-2078)

Merged
SA
2mo ago
+1132-58

Review round (Satya)

  • Structure: the publish command now follows the repo guidelines — command.ts orchestrates; utils/{collectDirectoryPublish,resolveEntryPath,registerWatch,uploadAssets,publishResult}/ each own one unit with barrel + co-located tests.
  • One result contract: publishResult is 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/watching always present in the JSON.
  • Asset ownership hardening: verifyPublishAssets now 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 an everyone page.
  • 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>/original object across versions via the manifest) — zero bytes move, no R2 copies needed.

Links

Summary

  • Directory publish: superset pages publish ./report/ — index.html is the page, every other file ships at its relative path and serves from the page's own origin with Range, 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_modules skipped, ≤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 new page.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 under versions/, files/, _superset/, ~, not index.html/thumbnail.jpg, no duplicates); every referenced file must be a ready file in the organization; attachments rows land in the same transaction as the version row; the manifest gains per-version assets: {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 with 206s, and an HTML asset carries the full page CSP like the document. style-src gains '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 .mov asset publishes with a codec warning

Testing

  • bun run typecheck — shared, trpc, mcp, cli, content, api, web
  • bun 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 206 ranges; 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.js routes 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.pull returns 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, and font-src gain '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.html and 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.
  • 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.