Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ node_modules
public
package-lock.json
**/._*
netlify/vault-data.enc.json
netlify/vault-proof.json
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
### Added

- **Source Vault** at `/vault/`: a password-gated page with every track's stems, the Suno style prompts in all five production styles (colour-coded), original and Suno-optimised lyrics (same words, shorter section tags), a one-click Remix kit, a Camelot harmonic map, and each full song with its measured length, BPM and key (and how many detection models agree). It opens with the site's rain intro decrypting the 20 files, uses the site's menu, logo draw-on and end credits, and runs the track-name rain in the gutters on wide screens.
- The access key, session and private content are handled by Netlify functions: the key is a Netlify environment variable, the session is a signed HttpOnly cookie, prompts/lyrics/analysis are committed only in encrypted form, and stems download through short-lived signed links from a private GitHub repo.
- Password-only: the prompts, lyrics and analysis are committed only in encrypted form, locked with the access key itself and opened in the browser. Stems download through short-lived signed links from a private GitHub repo, handed out by one small Netlify function to visitors who prove they know the key (one Netlify setting: `VAULT_GH_TOKEN`).

### Changed

Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,20 @@ gh release create v1.3.0 ARCHIVE_404.zip --title "ARCHIVE_404 · v1.3.0" --notes

A private page for collaborators: every track's stems, the Suno prompts in all five production styles, original and Suno-optimised lyrics, and each song with its measured length, BPM and key. It opens with the access key, then plays a rain intro that "decrypts" the 20 files.

The page itself is public code, so nothing private is in this repo in readable form:
The page is public code, so the private parts are locked:

| Piece | Where it lives |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Access key | Netlify environment variable `VAULT_KEY`, checked by `netlify/functions/vault-unlock.mjs` |
| Session | Signed, HttpOnly cookie (`VAULT_SECRET`), valid 7 days |
| Prompts, lyrics, analysis | `netlify/vault-data.enc.json`, AES-256-GCM encrypted with `VAULT_DATA_KEY`; decrypted only by `vault-data.mjs` for a signed-in visitor |
| Stems | Release `stems` on the private repo `error404website/error404-vault`; `vault-stem.mjs` hands out GitHub's 5-minute signed link (`VAULT_GH_TOKEN`, read-only) |
| Piece | How it's protected |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prompts, lyrics, analysis | `public/vault/data.enc.json`, AES-256-GCM with a key derived from **the access key itself** (PBKDF2-SHA256, 300,000 rounds). The browser opens it when the right key is typed; a wrong key can't. |
| Stem downloads | Release `stems` on the private repo `error404website/error404-vault`. `netlify/functions/vault-stems.mjs` hands out GitHub's short-lived signed link, only to a visitor who proves they know the key (checked against `netlify/vault-proof.json`, which holds just a hash). |

**Netlify environment variables** (Site configuration → Environment variables, scope Functions, mark as secret): `VAULT_KEY`, `VAULT_SECRET`, `VAULT_DATA_KEY`, `VAULT_STEMS_REPO`, `VAULT_STEMS_TAG`, `VAULT_GH_TOKEN`. Local copies live in the git-ignored `.env.vault`.
**The one Netlify setting:** `VAULT_GH_TOKEN`, a fine-grained GitHub token with access to error404-vault only and **Contents: Read-only** (Site configuration → Environment variables, scope Functions, secret). Without it the page works and stems show SOON.

**Updating the prompts or analysis:** edit the files in the git-ignored `vault-private/`, run `npm run vault:encrypt`, and commit the new `netlify/vault-data.enc.json`.
**Changing the key, prompts or analysis:** edit `.env.vault` (`VAULT_KEY=…`) or the files in `vault-private/` (both git-ignored), run `npm run vault:encrypt`, and commit the two regenerated files.

**Updating the stems:** upload the zips to the `stems` release on the private repo with the names `npm run vault:encrypt` prints (one per track, plus one per chapter for ALL STEMS, since GitHub caps a release file at 2 GB). Sizes appear in the vault automatically.
**Updating the stems:** upload the zips to the `stems` release on the private repo using the names `npm run vault:encrypt` prints (one per track, plus one per chapter for ALL STEMS, since GitHub caps a release file at 2 GB). Sizes appear in the vault automatically.

**Strength:** the key protects the files like a password on a zip. Someone could copy `data.enc.json` and try guesses offline, so use a long, unusual key if the contents need to stay secret from determined people.

### Notes

Expand Down
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[build.environment]
NODE_VERSION = "22"

# Source Vault server side: the key check, encrypted data and stem links (netlify/functions/vault-*.mjs)
# Source Vault: the stems function (netlify/functions/vault-stems.mjs; needs VAULT_GH_TOKEN)
[functions]
directory = "netlify/functions"
node_bundler = "esbuild"
Expand Down
14 changes: 0 additions & 14 deletions netlify/functions/vault-data.mjs

This file was deleted.

6 changes: 0 additions & 6 deletions netlify/functions/vault-lock.mjs

This file was deleted.

16 changes: 0 additions & 16 deletions netlify/functions/vault-stem.mjs

This file was deleted.

85 changes: 85 additions & 0 deletions netlify/functions/vault-stems.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// POST /api/vault/stems { proof, f? }
// no f → { ready, sizes } sizes of the zips on the private release
// f → { url, size } GitHub's short-lived signed link for one zip
//
// The stems live on release "stems" of the private repo error404website/error404-vault, read with the
// one Netlify setting this needs: VAULT_GH_TOKEN (fine-grained, that repo only, Contents: read).
// Visitors prove they know the access key with `proof` (a PBKDF2 of the key, derived in the browser),
// checked against netlify/vault-proof.json, which holds only its SHA-256.
import { createHash, timingSafeEqual } from "node:crypto";
import vault from "../vault-proof.json" with { type: "json" };

const REPO = "error404website/error404-vault";
const TAG = "stems";
const token = () =>
(typeof Netlify !== "undefined" ? Netlify.env.get("VAULT_GH_TOKEN") : process.env.VAULT_GH_TOKEN) || "";

const json = (body, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});

function proofOk(proof) {
const want = Buffer.from(vault.proof, "hex");
const got = createHash("sha256")
.update(String(proof || ""))
.digest();
return want.length === got.length && timingSafeEqual(want, got);
}

const gh = (path, init = {}) =>
fetch(`https://api.github.com${path}`, {
...init,
headers: {
authorization: `Bearer ${token()}`,
"x-github-api-version": "2022-11-28",
"user-agent": "error404-source-vault",
...(init.headers || {}),
},
});

let cache = { at: 0, assets: null };
async function assets() {
if (cache.assets && Date.now() - cache.at < 300e3) return cache.assets;
const res = await gh(`/repos/${REPO}/releases/tags/${TAG}`);
if (!res.ok) return {};
const rel = await res.json();
cache = {
at: Date.now(),
assets: Object.fromEntries(rel.assets.map((a) => [a.name, { id: a.id, size: a.size }])),
};
return cache.assets;
}

export default async (req) => {
if (req.method !== "POST") return json({ error: "method" }, 405);
let body = {};
try {
body = await req.json();
} catch {
return json({ error: "bad request" }, 400);
}
if (!proofOk(body.proof)) {
await new Promise((r) => setTimeout(r, 600));
return json({ error: "locked" }, 401);
}
if (!token()) return body.f ? json({ error: "not uploaded yet" }, 404) : json({ ready: false, sizes: {} });
const all = await assets().catch(() => ({}));
if (!body.f) {
const sizes = Object.fromEntries(Object.entries(all).map(([name, a]) => [name, a.size]));
return json({ ready: Object.keys(sizes).length > 0, sizes });
}
if (!vault.assets.includes(body.f)) return json({ error: "unknown file" }, 404);
const asset = all[body.f];
if (!asset) return json({ error: "not uploaded yet" }, 404);
// GitHub answers an asset download with a redirect to a signed URL that's valid for a few minutes
const res = await gh(`/repos/${REPO}/releases/assets/${asset.id}`, {
headers: { accept: "application/octet-stream" },
redirect: "manual",
});
const url = res.headers.get("location");
return url ? json({ url, size: asset.size }) : json({ error: "unavailable" }, 502);
};

export const config = { path: "/api/vault/stems" };
20 changes: 0 additions & 20 deletions netlify/functions/vault-unlock.mjs

This file was deleted.

1 change: 0 additions & 1 deletion netlify/vault-data.enc.json

This file was deleted.

116 changes: 0 additions & 116 deletions netlify/vault-lib.mjs

This file was deleted.

1 change: 1 addition & 0 deletions netlify/vault-proof.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"proof":"61cfcf5de89c4968f2bdca4a0e000bd8351212e3322afc707bb937fe6b8096ff","assets":["01_origin_stems.zip","02_left_behind_stems.zip","03_graft_stems.zip","04_impacted_stems.zip","05_empty_city_stems.zip","06_the_feed_stems.zip","07_gospel_out_stems.zip","08_endless_glow_stems.zip","09_awake_stems.zip","10_reclaimed_stems.zip","11_the_wreckage_stems.zip","12_read_stems.zip","13_come_home_stems.zip","14_changed_the_lock_stems.zip","15_down_the_front_stems.zip","16_whole_stems.zip","17_enough_stems.zip","18_open_sky_stems.zip","19_all_of_me_stems.zip","20_better_days_stems.zip","ARCHIVE_404_stems_01_origin.zip","ARCHIVE_404_stems_02_the_feed.zip","ARCHIVE_404_stems_03_the_wreckage.zip","ARCHIVE_404_stems_04_whole.zip"]}
1 change: 1 addition & 0 deletions public/vault/data.enc.json

Large diffs are not rendered by default.

Loading
Loading