mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 16:32:28 +00:00
Add a `release` job (main only, contents: write) that builds the archive and publishes it as an immutable GitHub Release tagged site-<date>-<shortsha>, marked --latest, with bitvid-site.zip attached. This gives a stable download URL for the current production archive: https://github.com/PR0M3TH3AN/bitvid/releases/latest/download/bitvid-site.zip Re-runs of the same commit refresh the asset instead of failing. The Release holds only the public compiled dist/ (same as Vercel serves) and no Nostr signing key — publishing to nsite stays a manual, client-signed step. Per-branch artifact upload is unchanged. Docs updated. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
361 lines
17 KiB
Markdown
361 lines
17 KiB
Markdown
# 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/
|
||
→ any branch: actions/upload-artifact → downloadable site .zip (30d)
|
||
→ main only: GitHub Release (site-<date>-<sha>, --latest) w/ bitvid-site.zip
|
||
stable URL: /releases/latest/download/bitvid-site.zip
|
||
(no Nostr signing key, does not publish to nsite)
|
||
|
||
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 holds **no Nostr signing key** and does not publish to nsite;
|
||
it only produces the compiled `dist/` for a manual publish:
|
||
|
||
- **Every branch** → uploads `dist/` as an artifact `bitvid-site-<branch>-<shortsha>`
|
||
(30-day retention). GitHub serves it as a `.zip` on download, `index.html` at the
|
||
root — drop-in for an nsite deployer. Grab the latest for a branch with
|
||
`gh run download` (see below).
|
||
- **`main` only** → additionally cuts an **immutable GitHub Release** (tag
|
||
`site-<date>-<shortsha>`, marked `--latest`) with `bitvid-site.zip` attached, giving
|
||
a **stable download URL** for the current production archive:
|
||
|
||
```
|
||
https://github.com/PR0M3TH3AN/bitvid/releases/latest/download/bitvid-site.zip
|
||
```
|
||
|
||
The Release contains only the public compiled app (identical to what Vercel serves),
|
||
so it's safe to expose permanently; it doubles as the per-release snapshot from D8.
|
||
|
||
Grab the latest archive for a non-prod branch:
|
||
|
||
```bash
|
||
gh run download -R PR0M3TH3AN/bitvid \
|
||
"$(gh run list -R PR0M3TH3AN/bitvid --workflow=nsite-archive.yml \
|
||
--branch unstable --status success -L1 --json databaseId -q '.[0].databaseId')" \
|
||
--dir ./site-archive
|
||
```
|
||
|
||
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).
|