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-<branch>-<shortsha>, 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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-14 09:29:41 -04:00
1 parent e81e65ff40
commit 10b93efa02
4 files changed
+389

No files matched your search

+49
View File
@@ -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
+3
View File
@@ -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
+336
View File
@@ -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://<npub>.<gateway>`. 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 `<npub>.<gateway>` 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=<nevent>`,
> `#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 `<npub>.<gateway>` 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://<bitvid-nsite-npub>.<gateway>
```
## 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-<branch>-<shortsha>` (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://<npub>.<gateway>`.
### 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/[email protected]
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 `<npub>.<gateway>`. 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).
+1
View File
@@ -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",