From 10b93efa02631bfa4a8770a653eca5c4b70373b4 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:29:41 -0400 Subject: [PATCH] ci(nsite): build a downloadable site archive on every push Add .github/workflows/nsite-archive.yml (push to any branch + workflow_dispatch): npm ci + npm run build, then upload dist/ as an artifact (bitvid-site--, 30d retention). GitHub serves it as a .zip with index.html at the root, ready for a manual nsite publish. The workflow holds no signing key and publishes nothing. Also add an `archive` npm script for the same output locally (bitvid-site.zip, now gitignored), and land docs/nsite-mirror-plan.md revised to the manual-publish + auto-archive model: sign client-side via NIP-07 or local nsyte, never an nsec/nbunksec in CI. The prior CI-signing workflow is retained as a labeled Phase-3 reference option. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV --- .github/workflows/nsite-archive.yml | 49 ++++ .gitignore | 3 + docs/nsite-mirror-plan.md | 336 ++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 389 insertions(+) create mode 100644 .github/workflows/nsite-archive.yml create mode 100644 docs/nsite-mirror-plan.md diff --git a/.github/workflows/nsite-archive.yml b/.github/workflows/nsite-archive.yml new file mode 100644 index 00000000..739ae376 --- /dev/null +++ b/.github/workflows/nsite-archive.yml @@ -0,0 +1,49 @@ +# Build a fresh, downloadable site archive on every push (any branch), so a +# ready-to-upload copy of the compiled app is always available for a MANUAL nsite +# publish (NIP-5A). This does NOT publish anything or touch any signing key — it +# just builds `dist/` and uploads it as a workflow artifact (GitHub serves +# artifacts as a .zip on download). See docs/nsite-mirror-plan.md. +name: Build site archive + +on: + push: + workflow_dispatch: + +permissions: + contents: read + +# One archive build in flight per branch; a newer push supersedes an older one. +concurrency: + group: nsite-archive-${{ github.ref }} + cancel-in-progress: true + +jobs: + archive: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build static site + run: npm run build + + - name: Short SHA + id: vars + run: echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + # `path: dist` stores the site files at the artifact root (index.html at top), + # so the downloaded .zip extracts straight to the nsite site directory. + - name: Upload site archive + uses: actions/upload-artifact@v4 + with: + name: bitvid-site-${{ github.ref_name }}-${{ steps.vars.outputs.sha }} + path: dist + if-no-files-found: error + retention-days: 30 diff --git a/.gitignore b/.gitignore index c0856d9f..ba2e6c75 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ TEST_LOG.mdtest_logs/nip-hits.txt # Vercel CLI local link/state .vercel + +# Local site archive for manual nsite upload (npm run archive) +bitvid-site.zip diff --git a/docs/nsite-mirror-plan.md b/docs/nsite-mirror-plan.md new file mode 100644 index 00000000..207157b0 --- /dev/null +++ b/docs/nsite-mirror-plan.md @@ -0,0 +1,336 @@ +# nsite mirror — censorship-resistant app-shell backup (NIP-5A) — Dev Plan + +Status: **DECISIONS LOCKED (D1–D8; D2 revised to manual-publish 2026-07-14)** — +the on-push **site-archive** workflow is shipped (`.github/workflows/nsite-archive.yml` ++ `npm run archive`); the actual nsite publish is a deliberate **manual** step the +maintainer runs when releasing (see "Maintainer steps"). Adds an +independently-recoverable mirror of bitvid's compiled static app on +**Nostr + Blossom**, published alongside Vercel. Full CI-signed publishing remains a +documented Phase-3 option below. + +Related: `docs/blossom-plan.md` (Blossom storage — nsite reuses the same blob +infra) and `docs/blossom-torrent-metadata-plan.md`. + +## Executive summary + +An **nsite** (NIP-5A) is a static website whose **identity + file map** live on +Nostr and whose **files** live on Blossom as content-addressed (sha256) blobs. A +signed Nostr manifest maps every path (`/index.html`, `/js/app.js`, …) to a blob +hash; any **gateway** reads the manifest from relays, fetches the blobs from +Blossom, and serves the site over HTTPS at e.g. `https://.`. Root +sites use **kind 15128**; a key can also host named sites with **kind 35128**. + +bitvid is a **fully static client** (verified: no `api/` dir, no Vercel functions, +`outputDirectory: "dist"`). So an nsite is not a "degraded emergency shell" — it is +a **complete, runnable copy of bitvid** that any gateway can serve. This is a +natural extension of bitvid's decentralization stack: + +| Layer | Decentralized by | +|---|---| +| Identity | Nostr (already) | +| Distribution | WebTorrent P2P (already) | +| Media storage | Blossom / R2 / S3 (already) | +| **App shell** | **nsite — this plan** | + +The mirror is a **disaster-recovery / censorship-resistance layer**, published +alongside — never replacing — the primary Vercel deploy. Git remains the +source-of-truth backup; nsite backs up *exactly what users run*, not how it was +built. + +## Why it fits bitvid (verified facts) + +- **Fully static** ⇒ the nsite is a full mirror, not a degraded shell. +- **Every file fits the Blossom free tier.** The largest file in a fresh `dist/` + is **1.85 MB** (a placeholder gif); everything else < 0.75 MB — all well under + Blossom's ~20 MiB free-per-blob cap. No paid Blossom tier needed for the mirror. +- **Reads work from any origin.** Nostr relays (wss), WebTorrent webseeds, and + Blossom reads are CORS-open, so a viewer at `.` gets a working + player. +- **Synergy with the Blossom work already shipped** — same blob servers, same + `blossom-client-sdk` mental model. + +## Decisions + +> **DECISION 1 — Role. ✅ LOCKED.** +> nsite is a **DR/mirror of the production (`main`) build**, not primary hosting. +> Vercel stays the polished public entry point on bitvid.network. +> _Rationale: parallel + independent; a Vercel outage/censorship still leaves a +> loadable bitvid on any gateway._ + +> **DECISION 2 — Publish trigger: MANUAL, with an automated archive. ✅ LOCKED +> (revised 2026-07-14).** +> The actual nsite publish is a **manual step** — a deliberate release ritual, not a +> push hook — so the signing key **never lives in CI**. To make that painless, a +> GitHub Action (`.github/workflows/nsite-archive.yml`) builds a **fresh, +> downloadable site archive on every push (any branch)** and uploads it as a +> workflow artifact (GitHub serves artifacts as a `.zip` on download). The +> maintainer grabs the latest archive (or runs `npm run archive` locally) and +> publishes it to an nsite deployer that signs **in-browser via NIP-07** (e.g. +> nsite.run) or via the local `nsyte` CLI. +> _Rationale: bitvid ships to prod manually and infrequently, and is aggressively +> key-safe — keeping the nsite key out of CI (no `nbunksec`, nothing to rotate/leak) +> matches that. Automating only the archive gives "always-ready to publish" without +> automating the signing. Never publish from Vercel's build command — a preview or +> rolled-back promotion could clobber the public mirror._ +> +> _Full CI publishing (a signing Action with an `nbunksec`) remains a Phase-3 option +> if the manual step ever becomes a chore — the manual flow validates everything +> first, so switching later is low-risk._ + +> **DECISION 3 — Directory. ✅ LOCKED.** +> Publish **`dist/`** (bitvid's `outputDirectory`, the exact bytes Vercel serves), +> after `npm ci && npm run build`. + +> **DECISION 4 — Identity. ✅ LOCKED.** +> A **dedicated bitvid nsite key** (NOT the personal or admin npub), **root site +> kind 15128**. Publish the npub + a canonical gateway URL so users/docs can find +> the mirror. +> _Rationale: project isolation, easy key rotation/hand-off, no exposure of a +> personal signing identity. Root (15128) is simplest for a single dedicated key +> (35128 named sites are for one key hosting several sites)._ + +> **DECISION 5 — Signing credential. ✅ LOCKED (revised 2026-07-14).** +> Default: **sign client-side, never in CI** — a NIP-07 extension (via a web +> deployer) or the local `nsyte` CLI with the dedicated nsite key on the maintainer's +> machine. **Never** put an `nsec` in CI. _If_ Phase-3 CI publishing is later +> adopted, use an **nbunksec** (restricted NIP-46 bunker credential) minted with +> `nsyte ci`, stored as the GitHub secret `NBUNK_SECRET`, rotated periodically with +> permitted kinds restricted where the bunker allows. + +> **DECISION 6 — Redundancy. ✅ LOCKED.** +> **≥3 Nostr relays + ≥2 Blossom servers**, `publishRelayList` + `publishServerList` +> on. Reuse Blossom servers bitvid already knows; add a **self-hosted Synology +> Blossom** as a personally-controlled copy later. +> _Rationale: one relay + one server is just a second centralized dependency._ + +> **DECISION 7 — SPA fallback. ✅ LOCKED.** +> `fallback: /index.html`. bitvid's routing is hash/query-based (`?v=`, +> `#view=…`), which resolves against `index.html`; the fallback covers gateways that +> 404 unknown deep-link paths. `embed.html` and `views/*.html` are distinct files +> served directly. + +> **DECISION 8 — Safety: secret scan + snapshots. ✅ LOCKED.** +> Keep the action's **secret scan ON** (`scan_level: medium`) as a guard even though +> bitvid's `dist/` is a public static client with no bundled secrets, and take an +> **immutable snapshot** per release so historical production builds stay +> addressable. Note: `dist/` currently includes source maps (`*.map`) — publishing +> them is harmless (they're already public) but the scan surfaces them. + +## The one real caveat — uploads & CORS + +The mirror **loads and plays** everywhere. But **uploads from a gateway origin** +depend on the target storage's CORS: + +- **Blossom uploads work** from the mirror (Blossom servers send `Access-Control- + Allow-Origin: *`). +- **R2 / S3 uploads will fail CORS** from `.` unless the user adds + that gateway origin to their bucket's CORS allow-list (buckets are normally + scoped to `bitvid.network`). + +So the mirror is positioned as a **resilient player / reader / DR layer** first; +full R2/S3 upload from the mirror is opt-in (add the gateway origin to bucket CORS). +Share/canonical links keep pointing at `bitvid.network` (`BITVID_WEBSITE_URL` / +`DNS_URL` are hardcoded) — which is correct, not a bug. + +## What nsite does and does NOT back up + +| Backs up | Does NOT back up | +|---|---| +| The compiled `dist/` (HTML/CSS/JS bundles, images, fonts, manifests) | Source + Git history (that's Git) | +| Exactly what users run | Env vars / credentials (none are in `dist/`) | +| Per-release snapshots (historical builds) | User videos (already on R2/S3/Blossom) | +| | Nostr events (already on relays) | + +## Architecture + +``` +push to any branch + ├── Vercel → production build → bitvid.network (primary, unchanged) + └── GitHub Action (.github/workflows/nsite-archive.yml) [SHIPPED] + npm ci && npm run build → dist/ + → actions/upload-artifact: downloadable site .zip (root = index.html) + (no signing key, publishes nothing) + +manual release (maintainer, when promoting to prod) + download latest archive (or: npm run archive → bitvid-site.zip) + → nsite deployer that signs in-browser via NIP-07 (e.g. nsite.run) + or the local `nsyte` CLI + → publish manifest (kind 15128) + blobs across Blossom servers + → reachable at https://. +``` + +## The archive workflow (shipped) + +`.github/workflows/nsite-archive.yml` — runs on **push to any branch** + manual +`workflow_dispatch`. It builds `dist/` and uploads it as an artifact named +`bitvid-site--` (30-day retention). GitHub serves the artifact as +a `.zip` on download, with `index.html` at the root — drop-in for an nsite deployer. +It holds **no secret** and publishes nothing. + +Locally, `npm run archive` produces the same thing as `bitvid-site.zip` (gitignored) +for an offline/manual publish. + +## The publish step (manual) + +Signing happens **off CI**, in the maintainer's control: + +1. Get the compiled site — download the latest `bitvid-site-*` artifact, or run + `npm run archive` locally. +2. Publish it with an nsite deployer that signs client-side: + - **Web**: an nsite uploader such as nsite.run, authorizing with a **NIP-07** + extension (the key never leaves the browser); or + - **CLI**: `nsyte deploy ./dist …` on the maintainer's machine (see recovery / + integrity commands below), signing with the dedicated nsite key locally. +3. Verify with `nsyte status --full` and by loading `https://.`. + +### Optional Phase-3: fully-automated CI publishing (reference only) + +If the manual step ever becomes a chore, a decoupled production-only Action can sign +in CI with a **restricted `nbunksec`** (NIP-46 bunker credential — **never an +`nsec`**). Not shipped; kept here as the migration target. The manual flow above +validates relays/servers/fallback first, so switching later is low-risk. + +```yaml +name: Publish nsite mirror +on: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: nsite-production + cancel-in-progress: true +jobs: + publish: + runs-on: ubuntu-latest + # Skip entirely until the maintainer adds NBUNK_SECRET. + if: ${{ github.repository == 'PR0M3TH3AN/bitvid' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22, cache: npm } + - run: npm ci + - run: npm run build + - name: Guard on secret + id: guard + run: echo "have=${{ secrets.NBUNK_SECRET != '' }}" >> "$GITHUB_OUTPUT" + - name: Publish to Nostr + Blossom + if: steps.guard.outputs.have == 'true' + uses: sandwichfarm/nsite-action@v0.5.1 + with: + nbunksec: ${{ secrets.NBUNK_SECRET }} + directory: ./dist + sync: true + fallback: /index.html + skip_secrets_scan: false + scan_level: medium + publish_relay_list: true + publish_server_list: true + relays: | + wss://relay.nsite.lol + wss://relay.damus.io + wss://nos.lol + servers: | + https://cdn.hzrd149.com + https://blossom.band +``` + +Pin the action + tool version once verified; curate relays/servers to ones whose +retention/payment policies are understood (do not blindly trust example public +servers). Add the Synology Blossom to `servers:` when it's up. + +## Security + +- **No secrets in `dist/`** — bitvid is a static client; `instance-config.js` + carries public flags/thresholds/npubs only. Secret scan stays on as a guard. +- **No signing key in CI (default).** The shipped archive workflow holds no secret; + publishing is signed client-side (NIP-07 or local `nsyte`). If Phase-3 CI + publishing is ever enabled, it signs with a restricted **`nbunksec`**, never an + `nsec`; a dedicated project key limits blast radius and eases rotation. +- **Public + immutable once replicated** — assume published blobs/manifests may + remain retrievable even after deletion requests (some relays ignore deletions). + Never publish a build containing anything you wouldn't put on a public CDN. +- **Gateway origin is untrusted infrastructure** — bitvid already treats relay/ + storage/tracker responses as untrusted; loading the shell from a gateway changes + the origin but not the trust model (all signing is client-side via the user's + own signer). + +## Recovery procedure + +If Vercel is lost: + +```bash +nsyte download ./recovered-site # pull the whole published build +cd recovered-site && python3 -m http.server 8080 # or re-host anywhere +# or fetch individual files: +nsyte get /index.html --output ./index.html +``` + +The mirror is therefore an **independently reconstructable production artifact**, +not merely a failover URL. + +## Integrity checks (post-release / weekly) + +```bash +npm run build +nsyte scan ./dist # catch accidental .env / sourcemaps / secrets +nsyte deploy ./dist --sync # replicate any missing blobs across servers +nsyte snapshot # immutable release snapshot +nsyte status --full # manifest + relay + server + per-file coverage +nsyte debug # relay discovery, manifest availability, sampled blob hashes +``` + +## Phases + +- **Phase 0 — Archive wiring (behavior-neutral). ✅ SHIPPED.** + `.github/workflows/nsite-archive.yml` (build + upload artifact on push to any + branch) + `npm run archive` + this doc. Publishes nothing; no secret involved. +- **Phase 1 — Go live (manual publish).** Maintainer generates the dedicated nsite + key, grabs the latest archive (or `npm run archive`), and publishes with a + client-side-signing deployer (nsite.run + NIP-07, or local `nsyte deploy`). Verify + with `nsyte status --full` and by loading `.`. Repeat on each prod + release. +- **Phase 2 — Harden.** Add the self-hosted Synology Blossom to the server list; run + periodic `nsyte debug` / `nsyte status --full` integrity checks; document/link the + mirror URL in bitvid (e.g. a "decentralized mirror" footer link). +- **Phase 3 — Reach / automate (optional).** Move signing into CI with a restricted + `nbunksec` (the reference workflow above) if manual publishing becomes a chore; + per-channel mirrors (a beta nsite); a named site (kind 35128); ENS/DNS `_nostr` + discovery. + +## Maintainer steps (only the human can do these) + +Archive automation is already live — these are the human-only publish steps: + +1. Generate a **dedicated bitvid nsite key** (not personal/admin). +2. Get the compiled site: download the latest `bitvid-site-*` GitHub artifact, or run + `npm run archive` locally (→ `bitvid-site.zip`). +3. Publish with a **client-side-signing** deployer — nsite.run authorized via a + **NIP-07** extension, or `nsyte deploy ./dist …` locally. The key stays on your + machine/extension; **do not** put an `nsec` (or `nbunksec`) in CI for the manual flow. +4. (Later) stand up a Synology Blossom server and add it to the server list. +5. (Optional) publish the npub + gateway URL so users can find the mirror. +6. (Optional, Phase 3) if automating: `nsyte ci` → `nbunksec1…`, add it as the + `NBUNK_SECRET` GitHub secret, and enable the reference publish workflow. + +## Risks / watch-items + +- **Uploads from the gateway origin** need bucket CORS (R2/S3); Blossom uploads + work. Positioned as a player/DR layer first — documented above. +- **Gateway availability/trust** — mitigate with multiple relays + servers and by + publishing the relay/server lists so any gateway can resolve the site. +- **Blob/relay retention** — free servers may prune; the self-hosted Synology + Blossom + periodic `--sync` keep coverage. `nsyte status --full` reports gaps. +- **Build determinism** — the mirror must publish the same `npm run build` output + Vercel serves; the Action rebuilds from the same `main` commit, so they match. +- **Source maps in `dist/`** — harmless to publish (already public); strip in build + later if undesired. +- **CI credential leakage** — restricted `nbunksec` in the secret manager, rotated; + a dedicated key caps the blast radius. + +## Sources + +- NIP-5A (nsite / Blossom static sites), kinds 15128 / 35128. +- `nsyte` CLI + `sandwichfarm/nsite-action` (deploy, snapshot, status, debug, ci). +- `docs/blossom-plan.md` (shared Blossom infrastructure). diff --git a/package.json b/package.json index caa1130d..3f0404fb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "prepare": "npm run build:css", "serve": "npx serve", "build": "node scripts/build-dist.mjs && npm run verify:dist:deploy-artifact", + "archive": "npm run build && rm -f bitvid-site.zip && (cd dist && zip -qr ../bitvid-site.zip .) && echo 'Wrote bitvid-site.zip (upload to your nsite deployer).'", "build:css": "postcss css/tailwind.source.css -o css/tailwind.generated.css", "build:beacon:bundle": "node scripts/build-beacon.mjs", "build:beacon": "npm run build:beacon:bundle && npm run lint:inline-styles",