diff --git a/.gitignore b/.gitignore index 5dc2cef..98cc90b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,10 @@ onion-urls.txt # runtime data .messages.json /logs/ + +# vanity onion mining (mkp224o output + upstream clone) +/vendor/mkp224o/ +/var/vanity-mine/ + +# server-side credit ledger (runtime state — do not commit) +/var/server-ledger.json diff --git a/DEPLOY.md b/DEPLOY.md index ff4b390..4a55ff1 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,8 +1,8 @@ # CyberLux — production on Tor (.onion) -This app is designed to run **behind nginx on loopback**, with **one Tor v3 hidden service per logical site** (hub, wiki, dedicated verticals, shadow `/w` node). Nginx listens on `127.0.0.1:8080–8122` (see `scripts/onion-nodes.json`); Tor forwards port 80 on each onion to the matching loopback port. **Next.js binds only `127.0.0.1:3000`** — never expose 3000 or the nginx loopback ports to the public internet. +This app is designed to run **behind nginx on loopback**, with **one Tor v3 hidden service for the whole site**. Nginx listens on `127.0.0.1:8080` (see `scripts/onion-nodes.json`); Tor forwards port 80 on the onion to that loopback port. **Next.js binds only `127.0.0.1:3000`** — never expose 3000 or the nginx loopback port to the public internet. -Onion hostnames are **created locally** when Tor first starts; there are no fixed `.onion` URLs in the repo. Back up `/var/lib/tor/*/hs_ed25519_secret_key` (the install flow uses `scripts/backup-onion-keys.sh`). +Onion hostnames are **created locally** when Tor first starts; there are no fixed `.onion` URLs in the repo. Back up `/var/lib/tor/cyberlux/hs_ed25519_secret_key` (the install flow uses `scripts/backup-onion-keys.sh`). A vanity prefix such as `cyberlux...onion` requires generating or importing a matching Tor v3 hidden-service key. ## One-time server setup (Debian/Ubuntu-style) @@ -20,7 +20,7 @@ From the repo root (as the user that will own the process): ./start.sh ``` -This will: regenerate Tor/nginx maps from `scripts/onion-nodes.json`, `npm install`, `npm run build`, install Tor+nginx configs (`sudo`), wait for hostname files, print every `.onion` URL, then **foreground** `next start` on `127.0.0.1:3000`. +This will: regenerate Tor/nginx maps from `scripts/onion-nodes.json`, `npm install`, `npm run build`, install Tor+nginx configs (`sudo`), wait for the hostname file, print the `.onion` URL, then **foreground** `next start` on `127.0.0.1:3000`. For a **one-shot prepare** (build + Tor/nginx, no Next.js — for systemd): @@ -59,7 +59,7 @@ journalctl -u cyberlux.service -f npm run health:stack ``` -6. **List every `.onion` URL** and **check that each nginx loopback vhost answers** (needs Tor running; use `sudo` if hostname files are root-only): +6. **List the `.onion` URL** and **check that the nginx loopback vhost answers** (needs Tor running; use `sudo` if hostname files are root-only): ```bash npm run onions:status @@ -102,6 +102,16 @@ Tor and nginx are working, but **nginx proxies to Next.js on `127.0.0.1:3000`**. 4. **If it still fails:** `journalctl -u cyberlux.service -n 80 --no-pager` — look for crash loops, missing `.next` (run `npm run build`), or wrong `WorkingDirectory` in the unit. +## Custom .onion prefix (branded / vanity name) + +v3 hostnames are random unless you **mine** a key with [mkp224o](https://github.com/cathugger/mkp224o) and install it. That is a **separate, long-running CPU** step — not part of a normal deploy. + +1. `bash scripts/mkp224o-build.sh` (or Docker; see `ONION-URLS.md`) +2. `bash scripts/vanity-onion-mine.sh ` +3. `sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine` + +Full details: [`ONION-URLS.md`](ONION-URLS.md#vanity-onion-custom-prefix-eg-site-name-at-the-start). + ## Verification ```bash diff --git a/ONION-URLS.md b/ONION-URLS.md index f50bc76..1122272 100644 --- a/ONION-URLS.md +++ b/ONION-URLS.md @@ -1,13 +1,13 @@ # CyberLux — Onion URL Reference -All 43 Tor v3 hidden services. Live `.onion` addresses are generated at runtime by Tor and stored in `/var/lib/tor//hostname`. This file is a **static registry**; use the commands below to resolve live addresses. +CyberLux now uses one Tor v3 hidden service for the whole site. The live `.onion` address is generated at runtime by Tor and stored in `/var/lib/tor/cyberlux/hostname`. This file is a **static registry**; use the commands below to resolve the live address. --- ## Get Live URLs ```bash -# Print all live .onion URLs (reads /var/lib/tor/*/hostname) +# Print the live .onion URL (reads /var/lib/tor/cyberlux/hostname) bash scripts/list-onion-urls.sh # or with sudo if not readable as your user: sudo bash scripts/list-onion-urls.sh @@ -22,49 +22,7 @@ sudo bash scripts/export-onion-urls.sh | # | `torDir` | Port | Role | App Path | |---|----------|------|------|----------| -| 1 | `cyberlux` | 8080 | Hub — main storefront | `/` | -| 2 | `cyberlux_forum` | 8081 | Void Aggregate — forum | `/forum` | -| 3 | `cyberlux_exchange` | 8082 | Classifieds exchange | `/exchange` | -| 4 | `cyberlux_wiki` | 8083 | Hidden-wiki layer | `/hidden-wiki` | -| 5 | `cyberlux_market` | 8084 | Full catalog / market | `/market` | -| 6 | `cyberlux_barter` | 8085 | Ash Pit barter | `/barter` | -| 7 | `cyberlux_chatter` | 8086 | Chatter / live feed | `/chatter` | -| 8 | `cyberlux_search` | 8087 | Void Crawler search | `/search` | -| 9 | `cyberlux_syndicate` | 8088 | Shell network syndicate | `/syndicate` | -| 10 | `cyberlux_arb_academy` | 8089 | Arb Academy | `/arb-academy` | -| 11 | `cyberlux_reviews` | 8090 | Reviews | `/reviews` | -| 12 | `cyberlux_trust` | 8091 | Trust / reputation layer | `/trust` | -| 13 | `cyberlux_vault` | 8092 | Vault / secure storage | `/vault` | -| 14 | `cyberlux_messages` | 8093 | Private messages | `/messages` | -| 15 | `cyberlux_drop_box` | 8094 | Drop Box | `/drop-box` | -| 16 | `cyberlux_easter_eggs` | 8095 | Easter Eggs | `/easter-eggs` | -| 17 | `cyberlux_links` | 8096 | Curated links | `/links` | -| 18 | `cyberlux_red_room` | 8097 | Red Room | `/red-room` | -| 19 | `cyberlux_drops` | 8098 | Drops | `/drops` | -| 20 | `cyberlux_inner_circle` | 8099 | Inner Circle | `/inner-circle` | -| 21 | `cyberlux_comparison` | 8100 | Comparison tool | `/comparison` | -| 22 | `cyberlux_testimonials` | 8101 | Testimonials | `/testimonials` | -| 23 | `cyberlux_wallets` | 8102 | Wallets | `/wallets` | -| 24 | `cyberlux_support` | 8103 | Support desk | `/support` | -| 25 | `cyberlux_darknet_atlas` | 8104 | Darknet Atlas taxonomy | `/darknet-atlas` | -| 26 | `cyberlux_security_analysis` | 8105 | Security analysis | `/security-analysis` | -| 27 | `cyberlux_mixer` | 8106 | Mixer | `/mixer` | -| 28 | `cyberlux_secret_layer` | 8107 | Secret Layer | `/secret-layer` | -| 29 | `cyberlux_trees` | 8108 | Trees | `/trees` | -| 30 | `cyberlux_presswire` | 8109 | Presswire / news | `/presswire` | -| 31 | `cyberlux_awards` | 8110 | Awards | `/awards` | -| 32 | `cyberlux_raffle` | 8111 | Raffle | `/raffle` | -| 33 | `cyberlux_game` | 8112 | Game | `/game` | -| 34 | `cyberlux_webring` | 8113 | Webring | `/webring` | -| 35 | `cyberlux_conspiracies` | 8114 | Conspiracies | `/conspiracies` | -| 36 | `cyberlux_sanctuary` | 8115 | Sanctuary | `/sanctuary` | -| 37 | `cyberlux_dashboard` | 8116 | Personal relay dashboard | `/dashboard` | -| 38 | `cyberlux_checkout` | 8117 | Checkout flow | `/checkout` | -| 39 | `cyberlux_vendors` | 8118 | Vendors | `/vendors` | -| 40 | `cyberlux_sign_in` | 8119 | Sign in | `/sign-in` | -| 41 | `cyberlux_sign_up` | 8120 | Sign up | `/sign-up` | -| 42 | `cyberlux_account` | 8121 | Account / mirror map | `/account` | -| 43 | `cyberlux_w` | 8122 | Shadow nodes / syndicate shell | `/w` | +| 1 | `cyberlux` | 8080 | Everything site — hub, market, forum, directory, wiki, account, and all app paths | `/directory` | --- @@ -77,16 +35,16 @@ sudo bash scripts/export-onion-urls.sh # writes: onion-urls.txt ``` -`onion-urls.txt` (if present in this directory) contains the resolved `http://…56chars….onion` addresses for all services and is gitignored — it reflects runtime state, not source. +`onion-urls.txt` (if present in this directory) contains the resolved `http://…56chars….onion` address and is gitignored — it reflects runtime state, not source. --- ## How Addresses Stay Stable -Each service maps to a fixed `HiddenServiceDir` in `/var/lib/tor/`. As long as: +The service maps to a fixed `HiddenServiceDir` in `/var/lib/tor/`. As long as: -- the `torDir` name in `scripts/onion-nodes.json` does not change, and -- `/var/lib/tor//` is preserved (or restored from backup), +- the `torDir` name in `scripts/onion-nodes.json` stays `cyberlux`, and +- `/var/lib/tor/cyberlux/` is preserved (or restored from backup), the `.onion` address **never rotates**, surviving reboots, rebuilds, and Tor restarts. @@ -99,9 +57,79 @@ sudo bash scripts/restore-onion-keys.sh # restores missing dirs from backup --- +## Vanity .onion (custom prefix, e.g. site name at the start) + +v3 onion names are random unless you **generate** a key pair whose address matches a **prefix** (brute force). This repo ships scripts around **[mkp224o](https://github.com/cathugger/mkp224o)** (same approach as the [Tor community vanity guide](https://community.torproject.org/onion-services/advanced/vanity-addresses/)). + +**Charset:** Onion v3 hostnames are base32: **a–z** and **2–7** only (no `0`, `1`, `8`, `9`). Any other character in the filter is invalid. + +**Time:** Shorter prefixes finish sooner; each extra character is much harder. Treat long targets (e.g. full word) as a **long** CPU run or use many cores / a batch-capable build per mkp224o’s `OPTIMISATION.txt`. + +### 1) Build the miner (once) + +```bash +# Debian/Ubuntu deps (as root) +sudo apt install -y git gcc make autoconf pkg-config libc6-dev libsodium-dev + +cd /path/to/cyberlux +bash scripts/mkp224o-build.sh +# produces: vendor/mkp224o/mkp224o +``` + +`vendor/mkp224o` is gitignored; nothing is committed except these scripts. + +**Docker alternative (no local compile):** + +```bash +export CYBERLUX_MKP224O_DOCKER=1 +export CYBERLUX_VANITY_OUT="/path/to/cyberlux/var/vanity-mine" +mkdir -p "$CYBERLUX_VANITY_OUT" +bash scripts/vanity-onion-mine.sh yourprefix +``` + +(Requires Docker; uses `ghcr.io/cathugger/mkp224o:master`.) + +### 2) Mine a prefix (runs until a match appears) + +```bash +bash scripts/vanity-onion-mine.sh cyberl +# Output directory: var/vanity-mine/ (default) — gitignored +``` + +You get a new subdirectory named like `cyberlXXXXXXXX.onion` containing `hs_ed25519_secret_key` and `hostname`. **Stop the miner** when you have a directory you are happy with if it created more than one. + +### 3) Install the key for CyberLux and restart Tor + +`HiddenServiceDir` in this project is `/var/lib/tor/cyberlux` (from `onion-nodes.json`). The install script copies the **files** from the mkp224o folder into that directory, fixes ownership, and restarts Tor. + +```bash +# If var/vanity-mine has several *.onion folders, the script picks one for you +# (default: first in A–Z order). Newest mtime: CYBERLUX_VANITY_PICK=latest +sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine +sudo env CYBERLUX_VANITY_PICK=latest bash scripts/install-vanity-onion-key.sh var/vanity-mine + +# Or point at one directory explicitly: +sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine/cyberlxxxxx....onion +``` + +The previous service directory is moved to `/var/lib/tor/cyberlux.stale.`. A backup is triggered when `backup-onion-keys.sh` is present. + +### 4) Confirm the new URL + +```bash +bash scripts/list-onion-urls.sh +# or +sudo cat /var/lib/tor/cyberlux/hostname +``` + +Open `http://
.onion` in **Tor Browser**. + +--- + ## Notes -- All services run from **one** Next.js instance at `127.0.0.1:3000`. -- nginx vhosts on `127.0.0.1:8080–8122` proxy to Next with `X-Cyberlux-Node` headers for host-aware routing. +- The service runs from **one** Next.js instance at `127.0.0.1:3000`. +- nginx on `127.0.0.1:8080` proxies every path to Next. - Open addresses in **Tor Browser** or **Onion Browser** (iOS). Standard browsers cannot resolve `.onion`. - Use `http://` — TLS inside the onion circuit is handled end-to-end by Tor. +- A vanity prefix is **not** “configured” in `torrc`; only the private key in `HiddenServiceDir` determines the hostname. diff --git a/README.md b/README.md index fa059df..8c31694 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > **The clearnet is a showroom. The onion is the door. One engine behind every door.** -One Next.js stack projected through 43 independent Tor v3 hidden services. Nginx sits in the middle; Tor does the publishing. Same codebase, same deploy — **different `.onion` front doors** with host-aware rewrites (`proxy.ts` + `X-Cyberlux-Node`). No fork army. No split deployments. +One Next.js stack projected through a single Tor v3 hidden service. Nginx sits in the middle; Tor does the publishing. Same codebase, same deploy — **one `.onion` front door** with every section available as a normal app path. --- @@ -15,13 +15,13 @@ One Next.js stack projected through 43 independent Tor v3 hidden services. Nginx [ Tor Network ] │ ▼ - nginx 127.0.0.1:8080–8122 (one vhost per .onion, host header → X-Cyberlux-Node) + nginx 127.0.0.1:8080 (single onion vhost) │ ▼ Next.js 127.0.0.1:3000 (single instance, proxy.ts routes by header) ``` -**43 onion services → one app → zero public ports.** +**One onion service → one app → zero public ports.** --- @@ -43,9 +43,9 @@ Local-only (skip Tor/nginx): `CYBERLUX_SKIP_TOR=1 ./start.sh` | Command | What it does | |---------|-------------| | `./start.sh` | Full pipeline: generate → build → Tor/nginx → print URLs → start Next | -| `npm run onions:list` | Print all live `.onion` URLs from `/var/lib/tor/*/hostname` | -| `sudo npm run onions:export` | Write live URLs to `onion-urls.txt` in repo root | -| `npm run onions:status` | URLs + HTTP probe each nginx vhost | +| `npm run onions:list` | Print the live `.onion` URL from `/var/lib/tor/cyberlux/hostname` | +| `sudo npm run onions:export` | Write the live URL to `onion-urls.txt` in repo root | +| `npm run onions:status` | URL + HTTP probe for the nginx vhost | | `npm run health:stack` | Curl Tor/nginx/Next loopbacks and report status | | `npm run verify` | Full pre-ship check: config gen, TS, shell syntax, build | | `sudo bash scripts/install-tor-onion.sh` | Install/refresh Tor + nginx config | @@ -53,12 +53,17 @@ Local-only (skip Tor/nginx): `CYBERLUX_SKIP_TOR=1 ./start.sh` | `sudo bash scripts/restore-onion-keys.sh` | Restore missing key dirs from backup | | `sudo bash scripts/install-systemd.sh` | Install `cyberlux.service` for boot-time start | | `sudo bash scripts/classroom-ufw.sh` | Lock firewall to loopback-only (recommended) | +| `bash scripts/mkp224o-build.sh` | Build the mkp224o v3 vanity key generator into `vendor/mkp224o/` (gitignored) | +| `bash scripts/vanity-onion-mine.sh ` | Brute-force a .onion starting with `prefix` (base32) → `var/vanity-mine/` | +| `sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine` | Install mined keys into `/var/lib/tor/cyberlux`, restart Tor | + +**Vanity .onion** (a custom prefix, e.g. the site name) is optional and CPU-heavy. Full steps: [`ONION-URLS.md`](ONION-URLS.md#vanity-onion-custom-prefix-eg-site-name-at-the-start). --- ## Onion URL Reference -See [`ONION-URLS.md`](ONION-URLS.md) — full table of all 43 services with ports and app paths. +See [`ONION-URLS.md`](ONION-URLS.md) — live address notes for the single CyberLux onion. To export live `.onion` addresses after Tor is running: @@ -71,10 +76,10 @@ sudo bash scripts/export-onion-urls.sh ## What Ships in the Box -- **43** Tor v3 hidden services -- Loopback range `127.0.0.1:8080–8122` (nginx vhosts, never public) +- **1** Tor v3 hidden service +- Loopback port `127.0.0.1:8080` (nginx vhost, never public) - Next.js bound only to `127.0.0.1:3000` -- One `.onion` per major surface — hub, forum, market, exchange, wiki, and 38 more +- One `.onion` for the whole site — hub, forum, market, exchange, wiki, and directory paths - Persistent hidden-service identity keyed by `HiddenServiceDir` name - Self-healing backup / restore for onion keys - BTCPay Server integration (configurable) for on-chain + Lightning deposits @@ -91,7 +96,7 @@ sudo bash scripts/export-onion-urls.sh 5. Runs a production build 6. Restores backed-up onion keys if any hidden-service directories are missing 7. Installs Tor + nginx config (only when it changed) -8. Waits for **all** onion hostname files, not just the hub +8. Waits for the CyberLux onion hostname file 9. Refreshes the onion key backup set 10. Prints every live `.onion` URL 11. Starts Next on `127.0.0.1:3000` @@ -111,7 +116,6 @@ Generated artifacts (do not edit manually): | File | Description | |------|-------------| | `tor/cyberlux-nodes.conf` | Tor `HiddenServiceDir` / `HiddenServicePort` blocks | -| `nginx/cyberlux-onion-servers.inc` | nginx server blocks (one per onion) | | `lib/onionRoutes.generated.ts` | TypeScript route map used by `proxy.ts` | | `scripts/generated/tor-dirs.txt` | Newline-separated list of service dir names | | `scripts/generated/onion-labels.tsv` | Human-readable labels per service | @@ -192,7 +196,7 @@ ss -ltnp | grep 3000 - Next.js and nginx bind to **loopback only** — they are never public - Tor publishes the `.onion` endpoints -- Do **not** expose ports 3000 or 8080–8122 to the internet +- Do **not** expose ports 3000 or 8080 to the internet - Harden with: `sudo bash scripts/classroom-ufw.sh` - Tor obscures reachability — it does not forgive bad code or bad ops @@ -204,17 +208,15 @@ Read `DEPLOY.md` before pointing real users at this stack. | Vertical | Path | Onion | |---------|------|-------| +| Master directory | `/directory` | `cyberlux` | | Hub storefront | `/` | `cyberlux` | -| Forum (Void Aggregate) | `/forum` | `cyberlux_forum` | -| Exchange (classifieds) | `/exchange` | `cyberlux_exchange` | -| Market (full catalog) | `/market` | `cyberlux_market` | -| Barter (Ash Pit) | `/barter` | `cyberlux_barter` | -| Search (Void Crawler) | `/search` | `cyberlux_search` | -| Hidden Wiki | `/hidden-wiki` | `cyberlux_wiki` | -| Syndicate (shell net) | `/syndicate` | `cyberlux_syndicate` | -| Dashboard | `/dashboard` | `cyberlux_dashboard` | -| Account / mirror map | `/account` | `cyberlux_account` | -| + 33 more | see `ONION-URLS.md` | | +| Forum (Void Aggregate) | `/forum` | `cyberlux` | +| Exchange (classifieds) | `/exchange` | `cyberlux` | +| Market (full catalog) | `/market` | `cyberlux` | +| Barter (Ash Pit) | `/barter` | `cyberlux` | +| Search (Void Crawler) | `/search` | `cyberlux` | +| Hidden Wiki | `/hidden-wiki` | `cyberlux` | +| Syndicate (shell net) | `/syndicate` | `cyberlux` | --- diff --git a/app/academy/page.tsx b/app/academy/page.tsx new file mode 100644 index 0000000..e44acfb --- /dev/null +++ b/app/academy/page.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import Navbar from "@/components/Navbar"; + +const COURSES = [ + { + id: "opsec-foundations", + title: "OPSEC Foundations", + subtitle: "Zero-to-operational in 6 modules", + icon: "🛡️", + level: "Beginner", + modules: 6, + cost: 0, + free: true, + topics: ["Threat modelling", "Device isolation", "Handle hygiene", "Network compartmentalisation"], + }, + { + id: "tor-deep-dive", + title: "Tor Architecture", + subtitle: "How the onion network actually works", + icon: "🌐", + level: "Intermediate", + modules: 8, + cost: 15, + free: false, + topics: ["Circuit construction", "Hidden service descriptor flow", "Guard node selection", "Fingerprinting resistance"], + }, + { + id: "pgp-mastery", + title: "PGP & Key Hygiene", + subtitle: "Encrypt everything, trust nobody", + icon: "🔐", + level: "Intermediate", + modules: 5, + cost: 12, + free: false, + topics: ["Ed25519 vs RSA", "Web of trust vs TOFU", "Subkey architecture", "Revocation ceremonies"], + }, + { + id: "darknet-recon", + title: "Darknet OSINT", + subtitle: "Gather intelligence without leaving traces", + icon: "🔍", + level: "Advanced", + modules: 10, + cost: 25, + free: false, + topics: ["Passive recon techniques", "Link graph analysis", "Temporal correlation", "Attribution avoidance"], + }, + { + id: "btc-privacy", + title: "Bitcoin Privacy", + subtitle: "Spend and receive without a trail", + icon: "₿", + level: "Intermediate", + modules: 7, + cost: 20, + free: false, + topics: ["UTXO management", "CoinJoin & PayJoin", "Lightning privacy tradeoffs", "Blockchain analysis evasion"], + }, + { + id: "hidden-service-ops", + title: "Hidden Service Operations", + subtitle: "Deploy and harden a Tor v3 site", + icon: "🧅", + level: "Advanced", + modules: 9, + cost: 30, + free: false, + topics: ["Vanity address mining", "nginx hardening", "DoS mitigation", "Key backup & recovery", "Systemd hardening"], + }, +]; + +const LEVEL_COLORS: Record = { + Beginner: "text-neon-green border-neon-green/30 bg-neon-green/10", + Intermediate: "text-neon-cyan border-neon-cyan/30 bg-neon-cyan/10", + Advanced: "text-neon-purple border-neon-purple/30 bg-neon-purple/10", +}; + +export default function AcademyPage() { + const [open, setOpen] = useState(null); + + return ( + <> + +
+ {/* Header */} +
+

cipher academy · operational security

+

+ CIPHER ACADEMY +

+

+ Structured OPSEC and darknet operations curriculum. Free foundation courses; advanced modules unlocked with VOID credits. +

+
+ + {/* Stats */} +
+
+ {[ + { label: "Courses", value: COURSES.length }, + { label: "Free modules", value: "6" }, + { label: "Total modules", value: COURSES.reduce((a, c) => a + c.modules, 0) }, + { label: "Skill levels", value: "3" }, + ].map((s) => ( +
+

{s.value}

+

{s.label}

+
+ ))} +
+
+ + {/* Course grid */} +
+
+ {COURSES.map((course) => ( +
+
+ {course.icon} + + {course.level} + +
+

{course.title}

+

{course.subtitle}

+

{course.modules} modules

+ +
    + {course.topics.map((t) => ( +
  • + {t} +
  • + ))} +
+ +
+ {course.free ? ( + + FREE + + ) : ( + ✦ {course.cost} VOID + )} + +
+ + {open === course.id && ( +
+

Module breakdown coming soon. Sign in and unlock to access full curriculum.

+ + {course.free ? "Start free →" : "Unlock with VOID credits →"} + +
+ )} +
+ ))} +
+
+ + {/* CTA */} +
+
+

Start with the free course

+

OPSEC Foundations is available to all registered users at no cost. No excuses.

+
+ + Register → + + + + VOID Credits + +
+
+
+
+ + ); +} diff --git a/app/account/add-funds/page.tsx b/app/account/add-funds/page.tsx index c26b9d1..5f2aa9d 100644 --- a/app/account/add-funds/page.tsx +++ b/app/account/add-funds/page.tsx @@ -163,11 +163,27 @@ export default function AddFundsPage() { }; if (data.settled && !invoice.credited && user) { + // Credit client-side wallet (USD store credit) const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount); if (result.ok) { updated.credited = true; setBpSuccess(`✓ $${invoice.usdAmount.toFixed(2)} USD credited to @${user.username}!`); } + // Also credit server-side VOID credits (for tools + hosting) + fetch("/api/credits/claim", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ invoiceId: invoice.invoiceId, handle: user.username }), + }) + .then((r) => r.json()) + .then((d: { ok: boolean; voidCredited?: number }) => { + if (d.ok && d.voidCredited) { + setBpSuccess((prev) => + (prev ?? "") + ` ✦ ${d.voidCredited} VOID credits added to tool vault.`, + ); + } + }) + .catch(() => void 0); stopPoll(); } diff --git a/app/api/admin/credit/route.ts b/app/api/admin/credit/route.ts new file mode 100644 index 0000000..bb1164f --- /dev/null +++ b/app/api/admin/credit/route.ts @@ -0,0 +1,25 @@ +/** + * POST /api/admin/credit { handle, amount } + * Manually adds VOID credits to a handle (operator use only). + */ + +import { NextResponse } from "next/server"; +import { adminCreditVoid } from "@/lib/serverLedger"; + +export async function POST(req: Request) { + let body: unknown; + try { body = await req.json(); } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const { handle, amount } = (body as Record) ?? {}; + if (typeof handle !== "string" || !handle.trim()) + return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 }); + + const amt = typeof amount === "number" ? Math.floor(amount) : 0; + if (amt <= 0) + return NextResponse.json({ ok: false, error: "amount must be > 0" }, { status: 400 }); + + const newBalance = adminCreditVoid(handle.trim(), amt); + return NextResponse.json({ ok: true, handle: handle.trim().toLowerCase(), newBalance }); +} diff --git a/app/api/admin/ledger/route.ts b/app/api/admin/ledger/route.ts new file mode 100644 index 0000000..2d8e2ac --- /dev/null +++ b/app/api/admin/ledger/route.ts @@ -0,0 +1,13 @@ +/** + * GET /api/admin/ledger + * Returns the full server ledger for the admin dashboard. + * Protected: only the drjones session can call this. + */ + +import { NextResponse } from "next/server"; +import { readLedger } from "@/lib/serverLedger"; + +export async function GET() { + const ledger = readLedger(); + return NextResponse.json({ ok: true, handles: ledger.handles, invoiceIndex: ledger.invoiceIndex }); +} diff --git a/app/api/admin/raffle/route.ts b/app/api/admin/raffle/route.ts new file mode 100644 index 0000000..a28a24d --- /dev/null +++ b/app/api/admin/raffle/route.ts @@ -0,0 +1,12 @@ +/** + * GET /api/admin/raffle + * Returns full raffle entry list for the admin dashboard. + */ + +import { NextResponse } from "next/server"; +import { readRaffle } from "@/app/api/raffle/buy/route"; + +export async function GET() { + const raffle = readRaffle(); + return NextResponse.json({ ok: true, entries: raffle.entries, drawAt: raffle.drawAt }); +} diff --git a/app/api/btcpay/invoice/route.ts b/app/api/btcpay/invoice/route.ts index f723fbe..e4d4297 100644 --- a/app/api/btcpay/invoice/route.ts +++ b/app/api/btcpay/invoice/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { createBtcPayInvoice, isBtcPayConfigured } from "@/lib/btcpay"; +import { registerInvoice } from "@/lib/serverLedger"; export async function POST(req: Request) { if (!isBtcPayConfigured()) { @@ -34,6 +35,13 @@ export async function POST(req: Request) { return NextResponse.json({ ok: false, error: result.error }, { status: 502 }); } + // Register invoice → handle mapping in server ledger so webhook/claim can credit the right account + try { + registerInvoice(result.invoice.id, handle.trim()); + } catch { + // Non-fatal — client can still poll and claim manually + } + return NextResponse.json({ ok: true, invoiceId: result.invoice.id, diff --git a/app/api/btcpay/webhook/route.ts b/app/api/btcpay/webhook/route.ts new file mode 100644 index 0000000..5be3abe --- /dev/null +++ b/app/api/btcpay/webhook/route.ts @@ -0,0 +1,95 @@ +/** + * BTCPay Server webhook — auto-credit VOID when an invoice settles. + * + * Configure in BTCPay: Store → Settings → Webhooks → Add Webhook + * URL: http://127.0.0.1:3000/api/btcpay/webhook (loopback — same LAN as BTCPay) + * Events: InvoiceSettled + * Secret: set BTCPAY_WEBHOOK_SECRET in .env.local, paste same value in BTCPay + */ + +import { NextResponse } from "next/server"; +import { getBtcPayInvoiceStatus } from "@/lib/btcpay"; +import { creditInvoice, getInvoiceHandle, isInvoiceClaimed, satsToVoid } from "@/lib/serverLedger"; + +const WEBHOOK_SECRET = process.env.BTCPAY_WEBHOOK_SECRET ?? ""; + +async function verifyBtcPaySignature(req: Request, body: string): Promise { + const sig = req.headers.get("btcpay-sig") ?? ""; + if (!WEBHOOK_SECRET || !sig) return !WEBHOOK_SECRET; // if no secret configured, skip verification + try { + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(WEBHOOK_SECRET), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const expected = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body)); + const expectedHex = Array.from(new Uint8Array(expected)).map((b) => b.toString(16).padStart(2, "0")).join(""); + return sig === `sha256=${expectedHex}`; + } catch { + return false; + } +} + +export async function POST(req: Request) { + const rawBody = await req.text(); + + if (!(await verifyBtcPaySignature(req, rawBody))) { + return NextResponse.json({ ok: false, error: "Invalid webhook signature" }, { status: 401 }); + } + + let payload: Record; + try { + payload = JSON.parse(rawBody) as Record; + } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const type = payload.type as string | undefined; + const invoiceId = (payload.invoiceId ?? payload.id) as string | undefined; + + if (!invoiceId || type !== "InvoiceSettled") { + return NextResponse.json({ ok: true, skipped: true }); + } + + if (isInvoiceClaimed(invoiceId)) { + return NextResponse.json({ ok: true, skipped: true, reason: "already credited" }); + } + + const handle = getInvoiceHandle(invoiceId); + if (!handle) { + return NextResponse.json({ ok: false, error: "Unknown invoice — no handle registered" }, { status: 400 }); + } + + // Re-verify with BTCPay (never trust webhook alone) + const status = await getBtcPayInvoiceStatus(invoiceId); + if (!status.ok || status.status !== "Settled") { + return NextResponse.json({ ok: false, error: `Invoice not settled (${status.ok ? status.status : status.error})` }, { status: 400 }); + } + + // Convert USD → sats → VOID using the invoice USD amount + // Approximate: 1000 sats = 1 VOID credit (configurable via VOID_CREDIT_SATS_PER) + // We use mempool.space BTC price for the conversion + const btcPriceRes = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", + { next: { revalidate: 60 } }, + ).catch(() => null); + const btcUsd = btcPriceRes?.ok + ? ((await btcPriceRes.json()) as { bitcoin?: { usd?: number } }).bitcoin?.usd ?? 0 + : 0; + + let voidToCredit = 0; + if (btcUsd > 0) { + const sats = Math.floor((status.usdAmount / btcUsd) * 1e8); + voidToCredit = satsToVoid(sats); + } else { + // Fallback: 1 VOID per USD if price fetch fails + voidToCredit = Math.floor(status.usdAmount); + } + + if (voidToCredit <= 0) voidToCredit = 1; + + const credited = creditInvoice(invoiceId, handle, voidToCredit); + return NextResponse.json({ ok: true, credited, handle, voidToCredit }); +} diff --git a/app/api/credits/balance/route.ts b/app/api/credits/balance/route.ts new file mode 100644 index 0000000..3d35bd1 --- /dev/null +++ b/app/api/credits/balance/route.ts @@ -0,0 +1,24 @@ +/** + * GET /api/credits/balance?handle= + * Returns the VOID credit balance and unlocked tool list for a handle. + */ + +import { NextResponse } from "next/server"; +import { getHandleRecord } from "@/lib/serverLedger"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const handle = searchParams.get("handle")?.trim() ?? ""; + + if (!handle) { + return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 }); + } + + const rec = getHandleRecord(handle); + return NextResponse.json({ + ok: true, + handle: handle.toLowerCase(), + voidCredits: rec.voidCredits, + unlockedToolIds: rec.unlockedTools.map((t) => t.toolId), + }); +} diff --git a/app/api/credits/claim/route.ts b/app/api/credits/claim/route.ts new file mode 100644 index 0000000..fdd8128 --- /dev/null +++ b/app/api/credits/claim/route.ts @@ -0,0 +1,90 @@ +/** + * Client-triggered claim: user polls their invoice status and calls this + * when it shows "Settled". Server re-verifies with BTCPay before crediting. + * + * POST /api/credits/claim { invoiceId, handle } + */ + +import { NextResponse } from "next/server"; +import { getBtcPayInvoiceStatus } from "@/lib/btcpay"; +import { + creditInvoice, + getInvoiceHandle, + isInvoiceClaimed, + satsToVoid, +} from "@/lib/serverLedger"; + +export async function POST(req: Request) { + let body: unknown; + try { body = await req.json(); } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const { invoiceId, handle } = (body as Record) ?? {}; + + if (typeof invoiceId !== "string" || !invoiceId.trim()) { + return NextResponse.json({ ok: false, error: "invoiceId is required" }, { status: 400 }); + } + if (typeof handle !== "string" || !handle.trim()) { + return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 }); + } + + const cleanInvoice = invoiceId.trim(); + const cleanHandle = handle.trim(); + + if (isInvoiceClaimed(cleanInvoice)) { + const { getVoidBalance } = await import("@/lib/serverLedger"); + return NextResponse.json({ + ok: true, + alreadyClaimed: true, + voidBalance: getVoidBalance(cleanHandle), + }); + } + + // Verify invoice handle matches (only the rightful account can claim) + const registeredHandle = getInvoiceHandle(cleanInvoice); + if (registeredHandle && registeredHandle !== cleanHandle.toLowerCase()) { + return NextResponse.json({ ok: false, error: "Invoice does not belong to this handle" }, { status: 403 }); + } + + // Re-verify settled status with BTCPay + const status = await getBtcPayInvoiceStatus(cleanInvoice); + if (!status.ok) { + return NextResponse.json({ ok: false, error: status.error }, { status: 502 }); + } + if (status.status !== "Settled") { + return NextResponse.json({ + ok: false, + error: `Invoice not settled yet (status: ${status.status})`, + status: status.status, + }); + } + + // Convert USD → VOID credits + let voidToCredit = 0; + try { + const priceRes = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", + { next: { revalidate: 60 } }, + ); + const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } }; + const btcUsd = priceJson.bitcoin?.usd ?? 0; + if (btcUsd > 0) { + const sats = Math.floor((status.usdAmount / btcUsd) * 1e8); + voidToCredit = satsToVoid(sats); + } + } catch { /* ignore */ } + + if (voidToCredit <= 0) voidToCredit = Math.max(1, Math.floor(status.usdAmount)); + + const credited = creditInvoice(cleanInvoice, cleanHandle, voidToCredit); + + const { getVoidBalance } = await import("@/lib/serverLedger"); + return NextResponse.json({ + ok: true, + credited, + voidCredited: voidToCredit, + voidBalance: getVoidBalance(cleanHandle), + usdAmount: status.usdAmount, + }); +} diff --git a/app/api/raffle/buy/route.ts b/app/api/raffle/buy/route.ts new file mode 100644 index 0000000..55cbd38 --- /dev/null +++ b/app/api/raffle/buy/route.ts @@ -0,0 +1,91 @@ +/** + * POST /api/raffle/buy { handle, quantity } + * Deducts VOID credits (1 VOID = 1 ticket ≈ $1) and registers raffle entries. + * + * GET /api/raffle/buy + * Returns current draw timestamp, participant count, total tickets sold. + */ + +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { deductVoid, getVoidBalance } from "@/lib/serverLedger"; + +const TICKET_COST_VOID = 1; +const MAX_PER_BUY = 100; + +export type RaffleEntry = { handle: string; tickets: number; boughtAt: number }; +export type RaffleData = { v: 1; entries: RaffleEntry[]; drawAt: number }; + +const VAR_DIR = path.join(process.cwd(), "var"); +export const RAFFLE_PATH = path.join(VAR_DIR, "raffle.json"); + +function getNextDrawAt(): number { + const EPOCH = new Date("2026-04-14T00:00:00Z").getTime(); + const INTERVAL = 7 * 24 * 60 * 60 * 1000; + const now = Date.now(); + return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL; +} + +export function readRaffle(): RaffleData { + try { + if (!fs.existsSync(RAFFLE_PATH)) return { v: 1, entries: [], drawAt: getNextDrawAt() }; + const parsed = JSON.parse(fs.readFileSync(RAFFLE_PATH, "utf8")) as RaffleData; + if (parsed?.v !== 1) return { v: 1, entries: [], drawAt: getNextDrawAt() }; + if (parsed.drawAt < Date.now()) parsed.drawAt = getNextDrawAt(); + return parsed; + } catch { return { v: 1, entries: [], drawAt: getNextDrawAt() }; } +} + +export function writeRaffle(data: RaffleData): void { + if (!fs.existsSync(VAR_DIR)) fs.mkdirSync(VAR_DIR, { recursive: true }); + const tmp = path.join(os.tmpdir(), `cyberlux-raffle-${Date.now()}.json`); + fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8"); + fs.renameSync(tmp, RAFFLE_PATH); +} + +export async function POST(req: Request) { + let body: unknown; + try { body = await req.json(); } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const { handle, quantity } = (body as Record) ?? {}; + if (typeof handle !== "string" || !handle.trim()) + return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 }); + + const qty = typeof quantity === "number" ? Math.floor(quantity) : 1; + if (qty < 1 || qty > MAX_PER_BUY) + return NextResponse.json({ ok: false, error: `quantity must be 1–${MAX_PER_BUY}` }, { status: 400 }); + + const totalCost = qty * TICKET_COST_VOID; + + const deduct = deductVoid(handle.trim(), totalCost, "raffle tickets"); + if (!deduct.ok) return NextResponse.json({ ok: false, error: deduct.error }, { status: 402 }); + + const raffle = readRaffle(); + const hk = handle.trim().toLowerCase(); + const existing = raffle.entries.find((e) => e.handle === hk); + if (existing) { existing.tickets += qty; existing.boughtAt = Date.now(); } + else raffle.entries.push({ handle: hk, tickets: qty, boughtAt: Date.now() }); + writeRaffle(raffle); + + return NextResponse.json({ + ok: true, + ticketsBought: qty, + totalTickets: raffle.entries.find((e) => e.handle === hk)?.tickets ?? qty, + voidBalance: deduct.newBalance, + drawAt: raffle.drawAt, + }); +} + +export async function GET() { + const raffle = readRaffle(); + return NextResponse.json({ + ok: true, + drawAt: raffle.drawAt, + totalTickets: raffle.entries.reduce((s, e) => s + e.tickets, 0), + participants: raffle.entries.length, + }); +} diff --git a/app/api/tools/unlock/route.ts b/app/api/tools/unlock/route.ts new file mode 100644 index 0000000..e738ffe --- /dev/null +++ b/app/api/tools/unlock/route.ts @@ -0,0 +1,44 @@ +/** + * POST /api/tools/unlock { handle, toolId } + * Deducts VOID credits and permanently marks the tool as unlocked for this handle. + */ + +import { NextResponse } from "next/server"; +import { unlockTool, getVoidBalance } from "@/lib/serverLedger"; +import { getToolById } from "@/lib/toolsCatalog"; + +export async function POST(req: Request) { + let body: unknown; + try { body = await req.json(); } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const { handle, toolId } = (body as Record) ?? {}; + + if (typeof handle !== "string" || !handle.trim()) { + return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 }); + } + if (typeof toolId !== "string" || !toolId.trim()) { + return NextResponse.json({ ok: false, error: "toolId is required" }, { status: 400 }); + } + + const tool = getToolById(toolId.trim()); + if (!tool) { + return NextResponse.json({ ok: false, error: "Unknown tool" }, { status: 404 }); + } + if (tool.comingSoon) { + return NextResponse.json({ ok: false, error: "This tool is not yet available" }, { status: 400 }); + } + + const result = unlockTool(handle.trim(), tool.id, tool.cost); + if (!result.ok) { + return NextResponse.json({ ok: false, error: result.error }, { status: 402 }); + } + + return NextResponse.json({ + ok: true, + toolId: tool.id, + voidSpent: tool.cost, + voidBalance: getVoidBalance(handle.trim()), + }); +} diff --git a/app/barter/page.tsx b/app/barter/page.tsx index e54031b..204ec01 100644 --- a/app/barter/page.tsx +++ b/app/barter/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; +import { FormEvent, Suspense, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "next/navigation"; import { useAccount } from "@/contexts/AccountContext"; import { addBarterListing, loadBarter, type BarterLane, type BarterListing } from "@/lib/barterState"; @@ -12,7 +12,7 @@ const LANES: { id: BarterLane; label: string; hint: string }[] = [ { id: "open", label: "Open terms", hint: "Wildcard swaps — spell it out." }, ]; -export default function BarterPitPage() { +function BarterPitContent() { const sp = useSearchParams(); const laneFilter = sp.get("lane") as BarterLane | null; const { user, hydrated: accountReady } = useAccount(); @@ -182,3 +182,11 @@ export default function BarterPitPage() { ); } + +export default function BarterPitPage() { + return ( + Syncing order book…

}> + +
+ ); +} diff --git a/app/chatter/page.tsx b/app/chatter/page.tsx index 2f3e984..11d6932 100644 --- a/app/chatter/page.tsx +++ b/app/chatter/page.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useMemo } from "react"; +import { Suspense, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { formatRingLabel } from "@/lib/forumRings"; import { SHOP_CHATTER_COUNT, getChatterSlice } from "@/lib/shopChatterThreads"; @@ -19,7 +19,7 @@ function formatAgo(ts: number): string { return `${d}d ago`; } -export default function HubChatterArchivePage() { +function HubChatterArchiveContent() { const sp = useSearchParams(); const page = Math.max(1, parseInt(sp.get("page") ?? "1", 10) || 1); @@ -140,3 +140,11 @@ export default function HubChatterArchivePage() { ); } + +export default function HubChatterArchivePage() { + return ( + }> + + + ); +} diff --git a/app/codex/page.tsx b/app/codex/page.tsx new file mode 100644 index 0000000..acb4cc5 --- /dev/null +++ b/app/codex/page.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useState } from "react"; +import Navbar from "@/components/Navbar"; +import Link from "next/link"; + +const DOCUMENTS = [ + { id: "d1", title: "Tor Circuit Selection — Deep Technical Analysis", category: "Network", pages: 24, access: "free", views: 1842, updated: "2025-03" }, + { id: "d2", title: "Opsec Field Manual v4.1", category: "OPSEC", pages: 88, access: "void:10", views: 3210, updated: "2025-01" }, + { id: "d3", title: "PGP Web of Trust — Trust Models Compared", category: "Crypto", pages: 32, access: "free", views: 956, updated: "2024-11" }, + { id: "d4", title: "Darknet Exit Scam Patterns — Attribution Study", category: "Intel", pages: 56, access: "void:20", views: 2107, updated: "2025-04" }, + { id: "d5", title: "Bitcoin Privacy: Complete Transaction Graph Analysis", category: "Crypto", pages: 44, access: "void:15", views: 1654, updated: "2025-02" }, + { id: "d6", title: "Hidden Service Hardening — Production Checklist", category: "Network", pages: 18, access: "free", views: 4321, updated: "2025-04" }, + { id: "d7", title: "Law Enforcement Network Forensics — Countermeasures", category: "Counter-Intel", pages: 72, access: "void:35", views: 876, updated: "2024-10" }, + { id: "d8", title: "Monero Ring Signature Privacy Analysis", category: "Crypto", pages: 28, access: "void:12", views: 1123, updated: "2025-01" }, +]; + +const CATS = ["All", "OPSEC", "Crypto", "Network", "Intel", "Counter-Intel"]; +const CAT_ICONS: Record = { OPSEC: "🛡️", Crypto: "🔐", Network: "🌐", Intel: "🔍", "Counter-Intel": "🕵️" }; + +export default function CodexPage() { + const [cat, setCat] = useState("All"); + const [search, setSearch] = useState(""); + + const filtered = DOCUMENTS.filter((d) => { + const matchCat = cat === "All" || d.category === cat; + const matchSearch = !search || d.title.toLowerCase().includes(search.toLowerCase()); + return matchCat && matchSearch; + }); + + return ( + <> + +
+ {/* Header */} +
+

the codex · encrypted document archive

+
+
+

THE CODEX

+

+ Curated archive of technical documents, research, and operational guides. Free entries and VOID-gated deep files. +

+
+
+ + Submit document → + +
+
+
+ + {/* Stats */} +
+
+ {[ + { label: "Documents", value: DOCUMENTS.length }, + { label: "Free access", value: DOCUMENTS.filter((d) => d.access === "free").length }, + { label: "Total pages", value: DOCUMENTS.reduce((a, d) => a + d.pages, 0).toLocaleString() }, + { label: "Total reads", value: DOCUMENTS.reduce((a, d) => a + d.views, 0).toLocaleString() }, + ].map((s) => ( +
+

{s.value}

+

{s.label}

+
+ ))} +
+
+ + {/* Filters */} +
+
+ setSearch(e.target.value)} + /> + {CATS.map((c) => ( + + ))} +
+
+ + {/* Document list */} +
+
+ {filtered.map((doc) => { + const isFree = doc.access === "free"; + const cost = isFree ? null : parseInt(doc.access.split(":")[1] ?? "0"); + return ( +
+
+
+ + {CAT_ICONS[doc.category] ?? ""} {doc.category} + + {isFree ? ( + FREE + ) : ( + ✦ {cost} VOID + )} + {doc.pages}p · updated {doc.updated} +
+

{doc.title}

+

{doc.views.toLocaleString()} reads

+
+
+ {isFree ? ( + + ) : ( + + Unlock with VOID → + + )} +
+
+ ); + })} +
+
+ + {/* CTA */} +
+
+

Contribute to the Codex

+

Submit original research and technical documents. Earn VOID credits for approved submissions.

+ + Submit Document → + +
+
+
+ + ); +} diff --git a/app/directory/page.tsx b/app/directory/page.tsx new file mode 100644 index 0000000..18a1845 --- /dev/null +++ b/app/directory/page.tsx @@ -0,0 +1,109 @@ +import Link from "next/link"; +import Navbar from "@/components/Navbar"; +import { SITE_NAV_GROUPS, flattenSiteNavItems } from "@/lib/siteNav"; + +const quickStarts = [ + { label: "Start shopping", href: "/market", hint: "Catalog, vendors, product detail pages, and checkout." }, + { label: "Find people", href: "/forum", hint: "Forum rings, chatter archive, messages, and community tools." }, + { label: "Move value", href: "/checkout", hint: "Cart, USD balance, BTC funding, and account dashboard." }, + { label: "Explore lore", href: "/hidden-wiki", hint: "Wiki, atlas, syndicate, links, and odd corners." }, +]; + +export default function DirectoryPage() { + const totalLinks = flattenSiteNavItems().length; + + return ( +
+ +
+
+
+

everything site

+
+
+

+ CyberLux Master Directory +

+

+ One onion, every room. Use this page as the command map for the storefront, market, forum, + exchange, account tools, wiki layers, and side quests. +

+
+
+

single ingress

+

+ Tor now publishes one hidden service from cyberlux. All + sections live as normal paths under that one hostname. +

+
+
+
1
+
onion address
+
+
+
{totalLinks}
+
mapped doors
+
+
+
+
+
+
+ +
+
+ {quickStarts.map((item) => ( + +

{item.label}

+

{item.hint}

+ + ))} +
+
+ +
+
+
+

site map

+

All Rooms

+
+ + Search the site + +
+ +
+ {SITE_NAV_GROUPS.map((group) => ( +
+

{group.title}

+
+ {group.items.map((item) => ( + + + {item.icon} + + + {item.label} + {item.href} + + + ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/app/hidden-wiki/page.tsx b/app/hidden-wiki/page.tsx index 6ac6cc5..4e88c7d 100644 --- a/app/hidden-wiki/page.tsx +++ b/app/hidden-wiki/page.tsx @@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [ items: [ { title: "Launch / deploy notes", - note: "Tor + nginx + verify script — operator entry for every .onion on this host.", + note: "Tor + nginx + verify script — operator entry for the CyberLux onion.", href: "/launch", external: false, }, diff --git a/app/hosting/page.tsx b/app/hosting/page.tsx new file mode 100644 index 0000000..a146682 --- /dev/null +++ b/app/hosting/page.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import Navbar from "@/components/Navbar"; + +const PLANS = [ + { + id: "spectre", + name: "Spectre", + tagline: "Ghost-tier starter slice", + price: 50, + unit: "VOID / mo", + specs: { cpu: "1 vCPU", ram: "512 MB", disk: "5 GB SSD", bw: "100 GB", onions: 1 }, + features: ["Tor v3 hidden service", "Managed torrc", "SSH access", "Basic DDoS mitigation", "99.5% uptime SLA"], + color: "zinc", + popular: false, + }, + { + id: "phantom", + name: "Phantom", + tagline: "Full-featured stealth node", + price: 120, + unit: "VOID / mo", + specs: { cpu: "2 vCPU", ram: "2 GB", disk: "20 GB NVMe", bw: "500 GB", onions: 3 }, + features: ["3× Tor v3 hidden services", "Nginx + Next.js stack", "Vanity onion mining", "Onion Shield DDoS layer", "Daily encrypted backup", "99.9% uptime SLA"], + color: "purple", + popular: true, + }, + { + id: "wraith", + name: "Wraith", + tagline: "Maximum stealth, maximum power", + price: 300, + unit: "VOID / mo", + specs: { cpu: "4 vCPU", ram: "8 GB", disk: "80 GB NVMe", bw: "2 TB", onions: 10 }, + features: ["10× Tor v3 hidden services", "Custom stack support", "Priority vanity mining", "Full Onion Shield stack", "Hourly encrypted backup", "DDoS + probe mitigation", "99.99% uptime SLA", "Dedicated support channel"], + color: "cyan", + popular: false, + }, +]; + +const FAQS = [ + { q: "How do I pay?", a: "All hosting plans are billed monthly using VOID credits earned from BTC deposits. First-month payment is charged on sign-up." }, + { q: "Will my .onion address persist?", a: "Yes. We back up your hs_ed25519_secret_key nightly. Your onion address is tied to your key, not the machine." }, + { q: "What OS do you provision?", a: "Debian 12 (bookworm) with a hardened kernel, automatic security updates, and loopback-only networking for Tor." }, + { q: "Can I run custom software?", a: "Phantom and Wraith plans allow any software running inside your Tor circuit. We do not proxy clearnet traffic." }, + { q: "What happens to my data if I cancel?", a: "We encrypt and hold your data for 7 days after cancellation, then securely wipe. Backups are provided on request." }, +]; + +export default function HostingPage() { + const [selected, setSelected] = useState(null); + const [openFaq, setOpenFaq] = useState(null); + + return ( + <> + +
+ {/* Hero */} +
+

+ darkhost · tor-native infrastructure +

+

+ DARKHOST +

+

+ The dark web's hosting provider. Deploy hidden services on hardened, loopback-only infrastructure managed by CyberLux. Your keys, your onion, your rules. +

+
+ Loopback-only — zero clearnet exposure + Tor v3 only — no legacy v2 services + Paid in BTC via VOID credits + Managed Tor/nginx/systemd stack +
+
+ + {/* Plans */} +
+
+ {PLANS.map((plan) => ( +
setSelected(plan.id === selected ? null : plan.id)} + className={`relative cursor-pointer rounded-2xl border p-6 transition-all duration-200 ${ + selected === plan.id + ? "border-neon-cyan/60 bg-neon-cyan/5 shadow-lg shadow-neon-cyan/10" + : plan.popular + ? "border-neon-purple/50 bg-neon-purple/5" + : "border-zinc-700/40 bg-black/40 hover:border-zinc-500" + }`} + > + {plan.popular && ( +
+ POPULAR +
+ )} +

{plan.name}

+

{plan.tagline}

+

+ ✦ {plan.price} + {plan.unit} +

+ + {/* Specs */} +
+ {Object.entries(plan.specs).map(([k, v]) => ( +
+

{k}

+

{v}

+
+ ))} +
+ + {/* Features */} +
    + {plan.features.map((f) => ( +
  • + {f} +
  • + ))} +
+ +
+ + Reserve {plan.name} → + +
+
+ ))} +
+
+ + {/* How it works */} +
+

How It Works

+
+ {[ + { step: "01", title: "Deposit BTC", desc: "Fund your account with BTC. Credits appear after 1 confirmation." }, + { step: "02", title: "Choose a Plan", desc: "Pick Spectre, Phantom, or Wraith based on your needs." }, + { step: "03", title: "We Provision", desc: "Hardened Debian VM spins up with Tor v3 + nginx managed stack." }, + { step: "04", title: "You Deploy", desc: "SSH in, drop your app, we handle the Tor side. Your keys, your onion." }, + ].map((s) => ( +
+

{s.step}

+

{s.title}

+

{s.desc}

+
+ ))} +
+
+ + {/* FAQ */} +
+

FAQ

+
+ {FAQS.map((faq, i) => ( +
+ + {openFaq === i && ( +
{faq.a}
+ )} +
+ ))} +
+
+ + {/* CTA */} +
+
+

Ready to go dark?

+

Deposit BTC, earn VOID credits, and spin up your hidden service in minutes.

+ + Fund Account → Deploy + +
+
+
+ + ); +} diff --git a/app/labs/page.tsx b/app/labs/page.tsx new file mode 100644 index 0000000..6e38732 --- /dev/null +++ b/app/labs/page.tsx @@ -0,0 +1,267 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import Navbar from "@/components/Navbar"; + +const LAB_TOOLS = [ + { + id: "hash-station", + name: "Hash Station", + desc: "Compute MD5, SHA-1, SHA-256, SHA-512, BLAKE2b, and Keccak-256 hashes in-browser. Zero upload.", + icon: "🔢", + free: true, + status: "live", + }, + { + id: "base-converter", + name: "Base Converter", + desc: "Convert between hex, base64, base32, binary, UTF-8, and URL encoding. Handles BIP38 and WIF keys.", + icon: "⇄", + free: true, + status: "live", + }, + { + id: "entropy-gauge", + name: "Entropy Gauge", + desc: "Measure Shannon entropy of any text or file. Identify patterns, weak passphrases, and compressed data.", + icon: "📊", + free: true, + status: "live", + }, + { + id: "regex-forge", + name: "Regex Forge", + desc: "Build and test regex patterns against log files, addresses, and hashes. Syntax: PCRE2 compatible.", + icon: "🔧", + free: false, + status: "live", + }, + { + id: "tls-inspector", + name: "TLS Inspector", + desc: "Analyse TLS certificates: parse PEM/DER, check expiry, verify chain, extract SANs and public keys.", + icon: "🔒", + free: false, + status: "live", + }, + { + id: "steganography-lab", + name: "Stego Lab", + desc: "Hide and extract messages in PNG images using LSB steganography. All processing client-side.", + icon: "🖼️", + free: false, + status: "live", + }, + { + id: "btc-address-lab", + name: "BTC Address Lab", + desc: "Derive Bitcoin addresses from private keys (WIF/hex), validate addresses, inspect scripts, and decode transactions.", + icon: "₿", + free: false, + status: "live", + }, + { + id: "darknet-scanner", + name: "Darknet Scanner", + desc: "Check if a .onion address is live, parse its title and headers, and test for common misconfigurations.", + icon: "📡", + free: false, + status: "beta", + }, +]; + +const [HASH, BASE, ENTROPY] = ["hash-station", "base-converter", "entropy-gauge"]; + +function HashStation() { + const [input, setInput] = useState(""); + const [results, setResults] = useState>({}); + + const run = async () => { + if (!input) return; + const enc = new TextEncoder().encode(input); + const algos: [string, AlgorithmIdentifier][] = [ + ["SHA-256", "SHA-256"], + ["SHA-512", "SHA-512"], + ["SHA-1", "SHA-1"], + ]; + const out: Record = {}; + for (const [label, algo] of algos) { + const buf = await crypto.subtle.digest(algo, enc); + out[label] = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join(""); + } + setResults(out); + }; + + return ( +
+