Replace remote with local repo

This commit is contained in:
drjones
2026-04-26 22:28:40 -07:00
parent 04d64fb993
commit e7c9da944e
57 changed files with 3658 additions and 3971 deletions

7
.gitignore vendored
View File

@@ -46,3 +46,10 @@ onion-urls.txt
# runtime data # runtime data
.messages.json .messages.json
/logs/ /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

View File

@@ -1,8 +1,8 @@
# CyberLux — production on Tor (.onion) # 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:80808122` (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) ## 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 ./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): 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 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 ```bash
npm run onions:status 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. 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 <base32_prefix>`
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 ## Verification
```bash ```bash

View File

@@ -1,13 +1,13 @@
# CyberLux — Onion URL Reference # 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/<service>/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 ## Get Live URLs
```bash ```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 bash scripts/list-onion-urls.sh
# or with sudo if not readable as your user: # or with sudo if not readable as your user:
sudo bash scripts/list-onion-urls.sh sudo bash scripts/list-onion-urls.sh
@@ -22,49 +22,7 @@ sudo bash scripts/export-onion-urls.sh
| # | `torDir` | Port | Role | App Path | | # | `torDir` | Port | Role | App Path |
|---|----------|------|------|----------| |---|----------|------|------|----------|
| 1 | `cyberlux` | 8080 | Hub — main storefront | `/` | | 1 | `cyberlux` | 8080 | Everything site — hub, market, forum, directory, wiki, account, and all app paths | `/directory` |
| 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` |
--- ---
@@ -77,16 +35,16 @@ sudo bash scripts/export-onion-urls.sh
# writes: onion-urls.txt # 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 ## 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 - the `torDir` name in `scripts/onion-nodes.json` stays `cyberlux`, and
- `/var/lib/tor/<service>/` is preserved (or restored from backup), - `/var/lib/tor/cyberlux/` is preserved (or restored from backup),
the `.onion` address **never rotates**, surviving reboots, rebuilds, and Tor restarts. 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: **az** and **27** 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 mkp224os `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 AZ 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.<timestamp>`. 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://<address>.onion` in **Tor Browser**.
---
## Notes ## Notes
- All services run from **one** Next.js instance at `127.0.0.1:3000`. - The service runs from **one** Next.js instance at `127.0.0.1:3000`.
- nginx vhosts on `127.0.0.1:80808122` proxy to Next with `X-Cyberlux-Node` headers for host-aware routing. - 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`. - 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. - 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.

View File

@@ -2,7 +2,7 @@
> **The clearnet is a showroom. The onion is the door. One engine behind every door.** > **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 ] [ Tor Network ]
nginx 127.0.0.1:80808122 (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) 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 | | Command | What it does |
|---------|-------------| |---------|-------------|
| `./start.sh` | Full pipeline: generate → build → Tor/nginx → print URLs → start Next | | `./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` | | `npm run onions:list` | Print the live `.onion` URL from `/var/lib/tor/cyberlux/hostname` |
| `sudo npm run onions:export` | Write live URLs to `onion-urls.txt` in repo root | | `sudo npm run onions:export` | Write the live URL to `onion-urls.txt` in repo root |
| `npm run onions:status` | URLs + HTTP probe each nginx vhost | | `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 health:stack` | Curl Tor/nginx/Next loopbacks and report status |
| `npm run verify` | Full pre-ship check: config gen, TS, shell syntax, build | | `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 | | `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/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/install-systemd.sh` | Install `cyberlux.service` for boot-time start |
| `sudo bash scripts/classroom-ufw.sh` | Lock firewall to loopback-only (recommended) | | `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 <prefix>` | 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 ## 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: 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 ## What Ships in the Box
- **43** Tor v3 hidden services - **1** Tor v3 hidden service
- Loopback range `127.0.0.1:80808122` (nginx vhosts, never public) - Loopback port `127.0.0.1:8080` (nginx vhost, never public)
- Next.js bound only to `127.0.0.1:3000` - 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 - Persistent hidden-service identity keyed by `HiddenServiceDir` name
- Self-healing backup / restore for onion keys - Self-healing backup / restore for onion keys
- BTCPay Server integration (configurable) for on-chain + Lightning deposits - 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 5. Runs a production build
6. Restores backed-up onion keys if any hidden-service directories are missing 6. Restores backed-up onion keys if any hidden-service directories are missing
7. Installs Tor + nginx config (only when it changed) 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 9. Refreshes the onion key backup set
10. Prints every live `.onion` URL 10. Prints every live `.onion` URL
11. Starts Next on `127.0.0.1:3000` 11. Starts Next on `127.0.0.1:3000`
@@ -111,7 +116,6 @@ Generated artifacts (do not edit manually):
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| `tor/cyberlux-nodes.conf` | Tor `HiddenServiceDir` / `HiddenServicePort` blocks | | `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` | | `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/tor-dirs.txt` | Newline-separated list of service dir names |
| `scripts/generated/onion-labels.tsv` | Human-readable labels per service | | `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 - Next.js and nginx bind to **loopback only** — they are never public
- Tor publishes the `.onion` endpoints - Tor publishes the `.onion` endpoints
- Do **not** expose ports 3000 or 80808122 to the internet - Do **not** expose ports 3000 or 8080 to the internet
- Harden with: `sudo bash scripts/classroom-ufw.sh` - Harden with: `sudo bash scripts/classroom-ufw.sh`
- Tor obscures reachability — it does not forgive bad code or bad ops - 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 | | Vertical | Path | Onion |
|---------|------|-------| |---------|------|-------|
| Master directory | `/directory` | `cyberlux` |
| Hub storefront | `/` | `cyberlux` | | Hub storefront | `/` | `cyberlux` |
| Forum (Void Aggregate) | `/forum` | `cyberlux_forum` | | Forum (Void Aggregate) | `/forum` | `cyberlux` |
| Exchange (classifieds) | `/exchange` | `cyberlux_exchange` | | Exchange (classifieds) | `/exchange` | `cyberlux` |
| Market (full catalog) | `/market` | `cyberlux_market` | | Market (full catalog) | `/market` | `cyberlux` |
| Barter (Ash Pit) | `/barter` | `cyberlux_barter` | | Barter (Ash Pit) | `/barter` | `cyberlux` |
| Search (Void Crawler) | `/search` | `cyberlux_search` | | Search (Void Crawler) | `/search` | `cyberlux` |
| Hidden Wiki | `/hidden-wiki` | `cyberlux_wiki` | | Hidden Wiki | `/hidden-wiki` | `cyberlux` |
| Syndicate (shell net) | `/syndicate` | `cyberlux_syndicate` | | Syndicate (shell net) | `/syndicate` | `cyberlux` |
| Dashboard | `/dashboard` | `cyberlux_dashboard` |
| Account / mirror map | `/account` | `cyberlux_account` |
| + 33 more | see `ONION-URLS.md` | |
--- ---

193
app/academy/page.tsx Normal file
View File

@@ -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<string, string> = {
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<string | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-14 text-center">
<p className="mb-3 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">cipher academy · operational security</p>
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
CIPHER<span className="text-neon-cyan"> ACADEMY</span>
</h1>
<p className="mx-auto mt-5 max-w-xl text-lg text-foreground/60">
Structured OPSEC and darknet operations curriculum. Free foundation courses; advanced modules unlocked with VOID credits.
</p>
</section>
{/* Stats */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<div className="flex flex-wrap justify-center gap-8 text-center">
{[
{ 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) => (
<div key={s.label}>
<p className="font-orbitron text-3xl font-black text-neon-cyan">{s.value}</p>
<p className="mt-1 text-xs text-foreground/50 uppercase tracking-widest">{s.label}</p>
</div>
))}
</div>
</section>
{/* Course grid */}
<section className="container mx-auto max-w-6xl px-4">
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{COURSES.map((course) => (
<div
key={course.id}
className="rounded-xl border border-zinc-700/40 bg-black/40 p-5 flex flex-col hover:border-zinc-500 transition"
>
<div className="flex items-start justify-between gap-2 mb-3">
<span className="text-3xl">{course.icon}</span>
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider ${LEVEL_COLORS[course.level]}`}>
{course.level}
</span>
</div>
<h3 className="font-orbitron text-base font-bold">{course.title}</h3>
<p className="mt-1 text-xs text-foreground/50">{course.subtitle}</p>
<p className="mt-1 text-xs text-foreground/40">{course.modules} modules</p>
<ul className="mt-4 flex-1 space-y-1">
{course.topics.map((t) => (
<li key={t} className="text-xs text-foreground/60 flex gap-2">
<span className="text-neon-cyan/60"></span> {t}
</li>
))}
</ul>
<div className="mt-5 flex items-center justify-between">
{course.free ? (
<span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-3 py-1 text-xs font-bold text-neon-green">
FREE
</span>
) : (
<span className="font-mono text-sm font-bold text-neon-purple"> {course.cost} VOID</span>
)}
<button
onClick={() => setOpen(open === course.id ? null : course.id)}
className="rounded-lg border border-zinc-600 bg-white/5 px-4 py-1.5 text-xs hover:border-zinc-400 transition"
>
{open === course.id ? "Collapse" : "Preview"}
</button>
</div>
{open === course.id && (
<div className="mt-4 rounded-lg border border-zinc-700/40 bg-black/30 p-4">
<p className="text-xs text-foreground/60 mb-3">Module breakdown coming soon. Sign in and unlock to access full curriculum.</p>
<Link
href={course.free ? "/sign-up" : "/account/add-funds"}
className="block text-center rounded-lg bg-neon-cyan/15 border border-neon-cyan/30 px-4 py-2 text-xs font-bold text-neon-cyan hover:bg-neon-cyan/25"
>
{course.free ? "Start free →" : "Unlock with VOID credits →"}
</Link>
</div>
)}
</div>
))}
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-2xl px-4 mt-20 text-center">
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-10">
<h2 className="font-orbitron text-2xl font-bold mb-3">Start with the free course</h2>
<p className="text-foreground/60 mb-6">OPSEC Foundations is available to all registered users at no cost. No excuses.</p>
<div className="flex flex-wrap justify-center gap-3">
<Link href="/sign-up" className="rounded-full border border-neon-cyan/40 bg-neon-cyan/10 px-6 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20">
Register
</Link>
<Link href="/account/add-funds" className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-2.5 text-sm font-bold text-background hover:opacity-90">
+ VOID Credits
</Link>
</div>
</div>
</section>
</main>
</>
);
}

View File

@@ -163,11 +163,27 @@ export default function AddFundsPage() {
}; };
if (data.settled && !invoice.credited && user) { if (data.settled && !invoice.credited && user) {
// Credit client-side wallet (USD store credit)
const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount); const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount);
if (result.ok) { if (result.ok) {
updated.credited = true; updated.credited = true;
setBpSuccess(`$${invoice.usdAmount.toFixed(2)} USD credited to @${user.username}!`); 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(); stopPoll();
} }

View File

@@ -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<string, unknown>) ?? {};
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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { createBtcPayInvoice, isBtcPayConfigured } from "@/lib/btcpay"; import { createBtcPayInvoice, isBtcPayConfigured } from "@/lib/btcpay";
import { registerInvoice } from "@/lib/serverLedger";
export async function POST(req: Request) { export async function POST(req: Request) {
if (!isBtcPayConfigured()) { if (!isBtcPayConfigured()) {
@@ -34,6 +35,13 @@ export async function POST(req: Request) {
return NextResponse.json({ ok: false, error: result.error }, { status: 502 }); 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({ return NextResponse.json({
ok: true, ok: true,
invoiceId: result.invoice.id, invoiceId: result.invoice.id,

View File

@@ -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<boolean> {
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<string, unknown>;
try {
payload = JSON.parse(rawBody) as Record<string, unknown>;
} 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 });
}

View File

@@ -0,0 +1,24 @@
/**
* GET /api/credits/balance?handle=<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),
});
}

View File

@@ -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<string, unknown>) ?? {};
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,
});
}

View File

@@ -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<string, unknown>) ?? {};
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,
});
}

View File

@@ -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<string, unknown>) ?? {};
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()),
});
}

View File

@@ -1,6 +1,6 @@
"use client"; "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 { useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext"; import { useAccount } from "@/contexts/AccountContext";
import { addBarterListing, loadBarter, type BarterLane, type BarterListing } from "@/lib/barterState"; 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." }, { id: "open", label: "Open terms", hint: "Wildcard swaps — spell it out." },
]; ];
export default function BarterPitPage() { function BarterPitContent() {
const sp = useSearchParams(); const sp = useSearchParams();
const laneFilter = sp.get("lane") as BarterLane | null; const laneFilter = sp.get("lane") as BarterLane | null;
const { user, hydrated: accountReady } = useAccount(); const { user, hydrated: accountReady } = useAccount();
@@ -182,3 +182,11 @@ export default function BarterPitPage() {
</div> </div>
); );
} }
export default function BarterPitPage() {
return (
<Suspense fallback={<p className="font-mono text-sm text-[#5a7d62]">Syncing order book</p>}>
<BarterPitContent />
</Suspense>
);
}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useMemo } from "react"; import { Suspense, useMemo } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { formatRingLabel } from "@/lib/forumRings"; import { formatRingLabel } from "@/lib/forumRings";
import { SHOP_CHATTER_COUNT, getChatterSlice } from "@/lib/shopChatterThreads"; import { SHOP_CHATTER_COUNT, getChatterSlice } from "@/lib/shopChatterThreads";
@@ -19,7 +19,7 @@ function formatAgo(ts: number): string {
return `${d}d ago`; return `${d}d ago`;
} }
export default function HubChatterArchivePage() { function HubChatterArchiveContent() {
const sp = useSearchParams(); const sp = useSearchParams();
const page = Math.max(1, parseInt(sp.get("page") ?? "1", 10) || 1); const page = Math.max(1, parseInt(sp.get("page") ?? "1", 10) || 1);
@@ -140,3 +140,11 @@ export default function HubChatterArchivePage() {
</div> </div>
); );
} }
export default function HubChatterArchivePage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#050403] text-[#d4c4b0]" />}>
<HubChatterArchiveContent />
</Suspense>
);
}

147
app/codex/page.tsx Normal file
View File

@@ -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<string, string> = { 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 (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">the codex · encrypted document archive</p>
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">THE <span className="text-neon-purple">CODEX</span></h1>
<p className="mt-3 max-w-xl text-foreground/60">
Curated archive of technical documents, research, and operational guides. Free entries and VOID-gated deep files.
</p>
</div>
<div className="flex gap-3">
<Link href="/sign-in" className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-2 text-sm font-bold text-neon-purple hover:bg-neon-purple/20">
Submit document
</Link>
</div>
</div>
</section>
{/* Stats */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-6 text-sm">
{[
{ 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) => (
<div key={s.label} className="rounded-xl border border-zinc-800 bg-black/40 px-5 py-3">
<p className="font-bold text-foreground text-lg">{s.value}</p>
<p className="text-xs text-foreground/40">{s.label}</p>
</div>
))}
</div>
</section>
{/* Filters */}
<section className="container mx-auto max-w-6xl px-4 mb-6">
<div className="flex flex-wrap items-center gap-3">
<input
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-64 focus:border-neon-purple/40 outline-none"
placeholder="Search documents…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{CATS.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-purple/50 bg-neon-purple/20 text-neon-purple" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
>
{c in CAT_ICONS ? `${CAT_ICONS[c]} ` : ""}{c}
</button>
))}
</div>
</section>
{/* Document list */}
<section className="container mx-auto max-w-6xl px-4">
<div className="space-y-2">
{filtered.map((doc) => {
const isFree = doc.access === "free";
const cost = isFree ? null : parseInt(doc.access.split(":")[1] ?? "0");
return (
<div
key={doc.id}
className="flex flex-col gap-3 rounded-xl border border-zinc-700/40 bg-black/40 p-5 hover:border-zinc-500 transition sm:flex-row sm:items-center"
>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1.5">
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">
{CAT_ICONS[doc.category] ?? ""} {doc.category}
</span>
{isFree ? (
<span className="rounded-full border border-neon-green/30 bg-neon-green/10 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span>
) : (
<span className="rounded-full border border-neon-purple/30 bg-neon-purple/10 px-2 py-0.5 text-[9px] font-bold text-neon-purple"> {cost} VOID</span>
)}
<span className="text-[10px] text-foreground/30">{doc.pages}p · updated {doc.updated}</span>
</div>
<p className="font-semibold text-sm text-foreground">{doc.title}</p>
<p className="text-xs text-foreground/35 mt-0.5">{doc.views.toLocaleString()} reads</p>
</div>
<div className="shrink-0">
{isFree ? (
<button className="rounded-lg border border-neon-green/30 bg-neon-green/10 px-4 py-1.5 text-xs font-bold text-neon-green hover:bg-neon-green/20 transition">
Read
</button>
) : (
<Link href="/tools" className="rounded-lg border border-neon-purple/30 bg-neon-purple/10 px-4 py-1.5 text-xs font-bold text-neon-purple hover:bg-neon-purple/20 transition">
Unlock with VOID
</Link>
)}
</div>
</div>
);
})}
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-2xl px-4 mt-16 text-center">
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-8">
<h2 className="font-orbitron text-xl font-bold mb-3">Contribute to the Codex</h2>
<p className="text-sm text-foreground/60 mb-5">Submit original research and technical documents. Earn VOID credits for approved submissions.</p>
<Link href="/sign-in" className="inline-block rounded-full bg-gradient-to-r from-neon-purple to-neon-cyan px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
Submit Document
</Link>
</div>
</section>
</main>
</>
);
}

109
app/directory/page.tsx Normal file
View File

@@ -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 (
<main className="min-h-screen bg-[#050505] text-zinc-100">
<Navbar />
<section className="relative overflow-hidden px-4 pb-12 pt-32">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(0,255,255,0.16),_transparent_38%),radial-gradient(circle_at_80%_20%,_rgba(191,0,255,0.13),_transparent_30%)]" />
<div className="relative mx-auto max-w-6xl">
<p className="font-mono text-[10px] uppercase tracking-[0.45em] text-neon-cyan/70">everything site</p>
<div className="mt-4 grid gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-end">
<div>
<h1 className="font-orbitron text-4xl font-black uppercase leading-none md:text-6xl">
CyberLux Master <span className="text-neon-cyan">Directory</span>
</h1>
<p className="mt-6 max-w-2xl text-base leading-relaxed text-zinc-400 md:text-lg">
One onion, every room. Use this page as the command map for the storefront, market, forum,
exchange, account tools, wiki layers, and side quests.
</p>
</div>
<div className="rounded-3xl border border-neon-cyan/20 bg-black/50 p-6 shadow-2xl shadow-neon-cyan/5">
<p className="font-mono text-xs uppercase tracking-[0.25em] text-neon-green/80">single ingress</p>
<p className="mt-3 text-sm leading-relaxed text-zinc-400">
Tor now publishes one hidden service from <code className="text-neon-cyan">cyberlux</code>. All
sections live as normal paths under that one hostname.
</p>
<div className="mt-5 grid grid-cols-2 gap-3 text-center font-mono text-xs">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="text-2xl font-bold text-neon-cyan">1</div>
<div className="mt-1 text-zinc-500">onion address</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="text-2xl font-bold text-neon-purple">{totalLinks}</div>
<div className="mt-1 text-zinc-500">mapped doors</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-6xl px-4 py-8">
<div className="grid gap-4 md:grid-cols-4">
{quickStarts.map((item) => (
<Link
key={item.href}
href={item.href}
className="group rounded-3xl border border-white/10 bg-white/[0.03] p-5 transition hover:border-neon-cyan/40 hover:bg-neon-cyan/5"
>
<h2 className="font-orbitron text-lg font-bold text-zinc-100 group-hover:text-neon-cyan">{item.label}</h2>
<p className="mt-3 text-sm leading-relaxed text-zinc-500">{item.hint}</p>
</Link>
))}
</div>
</section>
<section className="mx-auto max-w-6xl px-4 pb-24 pt-8">
<div className="mb-6 flex flex-col gap-3 border-b border-white/10 pb-6 md:flex-row md:items-end md:justify-between">
<div>
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">site map</p>
<h2 className="mt-2 font-orbitron text-3xl font-bold">All Rooms</h2>
</div>
<Link
href="/search"
className="rounded-full border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/10"
>
Search the site
</Link>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{SITE_NAV_GROUPS.map((group) => (
<section key={group.title} className="rounded-3xl border border-white/10 bg-black/40 p-5">
<h3 className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-zinc-500">{group.title}</h3>
<div className="mt-4 grid gap-2 sm:grid-cols-2">
{group.items.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 rounded-2xl border border-white/5 bg-white/[0.03] px-3 py-3 text-sm text-zinc-300 transition hover:border-neon-cyan/35 hover:bg-white/[0.06] hover:text-neon-cyan"
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/5 text-base">
{item.icon}
</span>
<span>
<span className="block font-semibold">{item.label}</span>
<span className="block font-mono text-[10px] text-zinc-600">{item.href}</span>
</span>
</Link>
))}
</div>
</section>
))}
</div>
</section>
</main>
);
}

View File

@@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
items: [ items: [
{ {
title: "Launch / deploy notes", 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", href: "/launch",
external: false, external: false,
}, },

197
app/hosting/page.tsx Normal file
View File

@@ -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<string | null>(null);
const [openFaq, setOpenFaq] = useState<number | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Hero */}
<section className="container mx-auto max-w-6xl px-4 mb-16 text-center">
<p className="mb-3 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">
darkhost · tor-native infrastructure
</p>
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
DARK<span className="text-neon-cyan">HOST</span>
</h1>
<p className="mx-auto mt-5 max-w-2xl text-lg text-foreground/60">
The dark web&apos;s hosting provider. Deploy hidden services on hardened, loopback-only infrastructure managed by CyberLux. Your keys, your onion, your rules.
</p>
<div className="mt-6 flex flex-wrap justify-center gap-4 text-sm text-foreground/50">
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Loopback-only zero clearnet exposure</span>
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Tor v3 only no legacy v2 services</span>
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Paid in BTC via VOID credits</span>
<span className="flex items-center gap-2"><span className="text-neon-cyan"></span> Managed Tor/nginx/systemd stack</span>
</div>
</section>
{/* Plans */}
<section className="container mx-auto max-w-6xl px-4 mb-20">
<div className="grid gap-6 md:grid-cols-3">
{PLANS.map((plan) => (
<div
key={plan.id}
onClick={() => 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 && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-neon-purple px-4 py-0.5 text-[10px] font-bold tracking-widest text-background">
POPULAR
</div>
)}
<h2 className="font-orbitron text-2xl font-bold">{plan.name}</h2>
<p className="mt-1 text-xs text-foreground/50">{plan.tagline}</p>
<p className="mt-5 font-mono text-3xl font-bold text-neon-purple">
{plan.price}
<span className="ml-2 text-sm text-foreground/50">{plan.unit}</span>
</p>
{/* Specs */}
<div className="mt-5 grid grid-cols-2 gap-2 text-xs">
{Object.entries(plan.specs).map(([k, v]) => (
<div key={k} className="rounded-lg bg-white/5 px-3 py-2">
<p className="text-foreground/40 uppercase tracking-wider text-[9px]">{k}</p>
<p className="font-bold text-foreground">{v}</p>
</div>
))}
</div>
{/* Features */}
<ul className="mt-5 space-y-1.5">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2 text-xs text-foreground/70">
<span className="mt-0.5 text-neon-cyan"></span> {f}
</li>
))}
</ul>
<div className="mt-6">
<Link
href="/account/add-funds"
className={`block w-full rounded-lg py-2.5 text-center text-sm font-bold transition ${
plan.popular
? "bg-gradient-to-r from-neon-cyan to-neon-purple text-background hover:opacity-90"
: "border border-zinc-600 bg-white/5 text-foreground hover:border-zinc-400"
}`}
>
Reserve {plan.name}
</Link>
</div>
</div>
))}
</div>
</section>
{/* How it works */}
<section className="container mx-auto max-w-6xl px-4 mb-20">
<h2 className="font-orbitron mb-8 text-2xl font-bold text-center">How It Works</h2>
<div className="grid gap-6 sm:grid-cols-4">
{[
{ 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) => (
<div key={s.step} className="rounded-xl border border-zinc-800 bg-black/40 p-5">
<p className="font-orbitron text-3xl font-black text-neon-cyan/20">{s.step}</p>
<p className="mt-2 font-bold text-foreground">{s.title}</p>
<p className="mt-1 text-sm text-foreground/55">{s.desc}</p>
</div>
))}
</div>
</section>
{/* FAQ */}
<section className="container mx-auto max-w-3xl px-4 mb-16">
<h2 className="font-orbitron mb-6 text-2xl font-bold text-center">FAQ</h2>
<div className="space-y-2">
{FAQS.map((faq, i) => (
<div key={i} className="rounded-xl border border-zinc-800 bg-black/40 overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-sm font-semibold text-left"
>
{faq.q}
<span className="text-foreground/40">{openFaq === i ? "" : "+"}</span>
</button>
{openFaq === i && (
<div className="px-5 pb-4 text-sm text-foreground/60">{faq.a}</div>
)}
</div>
))}
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-2xl px-4 text-center">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-10">
<h2 className="font-orbitron text-2xl font-bold mb-3">Ready to go dark?</h2>
<p className="text-foreground/60 mb-6">Deposit BTC, earn VOID credits, and spin up your hidden service in minutes.</p>
<Link
href="/account/add-funds"
className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-3 font-bold text-background hover:opacity-90"
>
Fund Account Deploy
</Link>
</div>
</section>
</main>
</>
);
}

267
app/labs/page.tsx Normal file
View File

@@ -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<Record<string, string>>({});
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<string, string> = {};
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 (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5">
<textarea
className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none"
rows={4}
placeholder="Enter text to hash…"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button
onClick={run}
className="mt-3 rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30"
>
Hash
</button>
{Object.entries(results).length > 0 && (
<div className="mt-4 space-y-2">
{Object.entries(results).map(([k, v]) => (
<div key={k}>
<p className="text-[10px] uppercase tracking-widest text-foreground/40 mb-1">{k}</p>
<p className="font-mono text-xs text-neon-cyan break-all">{v}</p>
</div>
))}
</div>
)}
</div>
);
}
function BaseConverter() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [from, setFrom] = useState("text");
const [to, setTo] = useState("hex");
const convert = () => {
try {
let bytes: Uint8Array;
if (from === "text") bytes = new TextEncoder().encode(input);
else if (from === "hex") bytes = new Uint8Array(input.match(/.{1,2}/g)!.map((h) => parseInt(h, 16)));
else if (from === "base64") bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0));
else bytes = new TextEncoder().encode(input);
if (to === "hex") setOutput(Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
else if (to === "base64") setOutput(btoa(String.fromCharCode(...bytes)));
else if (to === "text") setOutput(new TextDecoder().decode(bytes));
else setOutput(Array.from(bytes).map((b) => b.toString(2).padStart(8, "0")).join(" "));
} catch {
setOutput("Conversion error — check input format");
}
};
const modes = ["text", "hex", "base64", "binary"];
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
<div className="flex gap-3 flex-wrap">
{(["from", "to"] as const).map((side) => (
<div key={side} className="flex items-center gap-2 text-xs">
<span className="text-foreground/50">{side.toUpperCase()}:</span>
{modes.map((m) => (
<button
key={m}
onClick={() => side === "from" ? setFrom(m) : setTo(m)}
className={`rounded px-2 py-1 border ${(side === "from" ? from : to) === m ? "border-neon-cyan/40 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 text-foreground/50 hover:border-zinc-500"}`}
>
{m}
</button>
))}
</div>
))}
</div>
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={3} placeholder="Input…" value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={convert} className="rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30">Convert</button>
{output && <p className="font-mono text-xs text-neon-cyan break-all bg-zinc-900 rounded p-3">{output}</p>}
</div>
);
}
function EntropyGauge() {
const [input, setInput] = useState("");
const score = (() => {
if (!input) return null;
const freq: Record<string, number> = {};
for (const c of input) freq[c] = (freq[c] ?? 0) + 1;
const n = input.length;
return -Object.values(freq).reduce((s, f) => { const p = f / n; return s + p * Math.log2(p); }, 0);
})();
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={4} placeholder="Paste text to measure entropy…" value={input} onChange={(e) => setInput(e.target.value)} />
{score !== null && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-foreground/50">Shannon entropy</span>
<span className={`font-mono font-bold text-sm ${score > 4.5 ? "text-neon-green" : score > 3 ? "text-yellow-400" : "text-red-400"}`}>{score.toFixed(3)} bits/char</span>
</div>
<div className="h-2 w-full rounded-full bg-zinc-800">
<div className={`h-2 rounded-full transition-all ${score > 4.5 ? "bg-neon-green" : score > 3 ? "bg-yellow-400" : "bg-red-400"}`} style={{ width: `${Math.min(100, (score / 6) * 100)}%` }} />
</div>
<p className="mt-2 text-xs text-foreground/40">{score > 4.5 ? "High entropy — good randomness" : score > 3 ? "Moderate — some patterns detected" : "Low entropy — likely weak or patterned"}</p>
</div>
)}
</div>
);
}
export default function LabsPage() {
const [active, setActive] = useState<string | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">void labs · browser-native security tools</p>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">VOID <span className="text-neon-cyan">LABS</span></h1>
<p className="mt-3 max-w-xl text-foreground/60">Privacy and security micro-tools that run entirely in your browser. No uploads. No logs. Free and premium tiers.</p>
</section>
<section className="container mx-auto max-w-6xl px-4 mb-10">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{LAB_TOOLS.map((t) => (
<div key={t.id} onClick={() => setActive(active === t.id ? null : t.id)}
className={`cursor-pointer rounded-xl border p-4 transition ${active === t.id ? "border-neon-cyan/50 bg-neon-cyan/5" : "border-zinc-700/40 bg-black/40 hover:border-zinc-500"}`}>
<div className="flex items-start justify-between mb-2">
<span className="text-2xl">{t.icon}</span>
<div className="flex gap-1">
{t.free ? <span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span> : <span className="rounded-full bg-neon-purple/15 border border-neon-purple/30 px-2 py-0.5 text-[9px] font-bold text-neon-purple">VOID</span>}
{t.status === "beta" && <span className="rounded-full bg-yellow-500/15 border border-yellow-500/30 px-2 py-0.5 text-[9px] font-bold text-yellow-400">BETA</span>}
</div>
</div>
<p className="font-semibold text-sm">{t.name}</p>
<p className="mt-1 text-xs text-foreground/50 leading-snug">{t.desc}</p>
</div>
))}
</div>
</section>
{/* Inline tools */}
{active && (
<section className="container mx-auto max-w-3xl px-4 mb-10">
<h2 className="font-orbitron text-lg font-bold mb-4">
{LAB_TOOLS.find((t) => t.id === active)?.name}
</h2>
{active === HASH && <HashStation />}
{active === BASE && <BaseConverter />}
{active === ENTROPY && <EntropyGauge />}
{active !== HASH && active !== BASE && active !== ENTROPY && (
<div className="rounded-xl border border-zinc-700/40 bg-black/40 p-8 text-center">
<p className="text-foreground/50 mb-4">This tool requires VOID credits to access.</p>
<Link href="/tools" className="rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
Unlock in VOID Tools
</Link>
</div>
)}
</section>
)}
<section className="container mx-auto max-w-2xl px-4 mt-10 text-center">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-8">
<h2 className="font-orbitron text-xl font-bold mb-3">Premium labs unlock</h2>
<p className="text-sm text-foreground/60 mb-5">Premium tools are included with the VOID Tools subscription. Deposit BTC to get VOID credits.</p>
<Link href="/account/add-funds" className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
+ Get VOID Credits
</Link>
</div>
</section>
</main>
</>
);
}

View File

@@ -11,8 +11,8 @@ export default function LaunchPage() {
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/80">deployment</p> <p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/80">deployment</p>
<h1 className="mt-2 font-orbitron text-4xl font-black text-white md:text-5xl">Launch on the dark web</h1> <h1 className="mt-2 font-orbitron text-4xl font-black text-white md:text-5xl">Launch on the dark web</h1>
<p className="mt-4 text-foreground/75"> <p className="mt-4 text-foreground/75">
Ship this stack as Tor v3 hidden services: nginx on loopback ports, Next.js on{" "} Ship this stack as one Tor v3 hidden service: nginx on loopback, Next.js on{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-neon-green">127.0.0.1:3000</code>, one onion per vertical. <code className="rounded bg-white/10 px-1.5 py-0.5 text-neon-green">127.0.0.1:3000</code>, every vertical as a path.
Repo ships generators and systemd hooks you bring the box and opsec. Repo ships generators and systemd hooks you bring the box and opsec.
</p> </p>
@@ -36,10 +36,10 @@ export default function LaunchPage() {
</section> </section>
<section className="mt-8 space-y-4 rounded-2xl border border-white/10 bg-black/40 p-8 backdrop-blur"> <section className="mt-8 space-y-4 rounded-2xl border border-white/10 bg-black/40 p-8 backdrop-blur">
<h2 className="font-orbitron text-lg font-bold text-neon-purple">3. Verify every onion & loopback</h2> <h2 className="font-orbitron text-lg font-bold text-neon-purple">3. Verify onion & loopback</h2>
<pre className="overflow-x-auto rounded-xl bg-black/60 p-4 text-xs text-neon-cyan/90"> <pre className="overflow-x-auto rounded-xl bg-black/60 p-4 text-xs text-neon-cyan/90">
{`npm run health:stack {`npm run health:stack
npm run onions:list # every .onion URL npm run onions:list # CyberLux .onion URL
sudo bash scripts/list-onion-urls.sh # if hostname files need root sudo bash scripts/list-onion-urls.sh # if hostname files need root
npm run onions:status # URLs + loopback checks`} npm run onions:status # URLs + loopback checks`}
</pre> </pre>

View File

@@ -3,7 +3,7 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react"; import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock"; import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
@@ -35,7 +35,7 @@ const SYM: Record<ShopCurrency, string> = {
type SortKey = "relevance" | "price-asc" | "price-desc" | "name"; type SortKey = "relevance" | "price-asc" | "price-desc" | "name";
export default function MarketPage() { function MarketContent() {
const sp = useSearchParams(); const sp = useSearchParams();
const router = useRouter(); const router = useRouter();
const initialCat = sp.get("category") ?? ""; const initialCat = sp.get("category") ?? "";
@@ -191,6 +191,14 @@ export default function MarketPage() {
); );
} }
export default function MarketPage() {
return (
<Suspense fallback={<div className="font-mono text-sm text-[#00ff41]/50">Loading market</div>}>
<MarketContent />
</Suspense>
);
}
function MarketProductCard({ function MarketProductCard({
product: p, product: p,
spotBtc, spotBtc,

150
app/nexus/page.tsx Normal file
View File

@@ -0,0 +1,150 @@
"use client";
import { useState } from "react";
import Navbar from "@/components/Navbar";
const NODES = [
{ id: "n1", name: "Void Exchange", onion: "voidexxxxx…onion", category: "Market", status: "up", uptime: "99.1%", last: "12s ago", guard: true },
{ id: "n2", name: "Phantom Library", onion: "phantomll…onion", category: "Wiki", status: "up", uptime: "97.4%", last: "2m ago", guard: false },
{ id: "n3", name: "Ghost Relay #7", onion: "ghostrly7…onion", category: "Relay", status: "up", uptime: "99.8%", last: "5s ago", guard: true },
{ id: "n4", name: "Ember Forum", onion: "emberfrum…onion", category: "Forum", status: "down", uptime: "81.2%", last: "4h ago", guard: false },
{ id: "n5", name: "Cipher Drop", onion: "ciphdrpxx…onion", category: "Drop", status: "up", uptime: "95.5%", last: "30s ago", guard: false },
{ id: "n6", name: "Zero Wiki", onion: "zerowikxx…onion", category: "Wiki", status: "degraded", uptime: "91.0%", last: "8m ago", guard: true },
{ id: "n7", name: "Dark Market", onion: "drkmarket…onion", category: "Market", status: "up", uptime: "98.3%", last: "18s ago", guard: true },
{ id: "n8", name: "Nexus Gate", onion: "nexusgte…onion", category: "Relay", status: "up", uptime: "99.9%", last: "2s ago", guard: true },
];
const STATUS_COLORS: Record<string, string> = {
up: "text-neon-green bg-neon-green/10 border-neon-green/30",
down: "text-red-400 bg-red-500/10 border-red-500/30",
degraded: "text-yellow-400 bg-yellow-500/10 border-yellow-500/30",
};
const STATUS_DOT: Record<string, string> = {
up: "bg-neon-green",
down: "bg-red-500",
degraded: "bg-yellow-400",
};
const CATS = ["All", "Market", "Wiki", "Forum", "Relay", "Drop"];
export default function NexusPage() {
const [cat, setCat] = useState("All");
const [search, setSearch] = useState("");
const filtered = NODES.filter((n) => {
const matchCat = cat === "All" || n.category === cat;
const matchSearch = !search || n.name.toLowerCase().includes(search.toLowerCase()) || n.onion.includes(search.toLowerCase());
return matchCat && matchSearch;
});
const up = NODES.filter((n) => n.status === "up").length;
const down = NODES.filter((n) => n.status === "down").length;
const degraded = NODES.filter((n) => n.status === "degraded").length;
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
<section className="container mx-auto max-w-6xl px-4 mb-10">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">nexus · hidden service network tracker</p>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
THE <span className="text-neon-cyan">NEXUS</span>
</h1>
<p className="mt-3 max-w-xl text-foreground/60">
Real-time monitoring of trusted hidden services across the Tor network. Submit your node to join the monitored ring.
</p>
</section>
{/* Status overview */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-4">
{[
{ label: "Online", count: up, color: "neon-green" },
{ label: "Degraded", count: degraded, color: "yellow-400" },
{ label: "Offline", count: down, color: "red-400" },
].map((s) => (
<div key={s.label} className="flex items-center gap-3 rounded-xl border border-zinc-700/40 bg-black/40 px-5 py-3">
<span className={`h-2.5 w-2.5 rounded-full bg-${s.color} animate-pulse`} />
<div>
<p className="text-xl font-bold font-orbitron">{s.count}</p>
<p className="text-xs text-foreground/40">{s.label}</p>
</div>
</div>
))}
</div>
</section>
{/* Filters */}
<section className="container mx-auto max-w-6xl px-4 mb-6">
<div className="flex flex-wrap items-center gap-3">
<input
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-56 focus:border-neon-cyan/40 outline-none"
placeholder="Search nodes…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="flex flex-wrap gap-2">
{CATS.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-cyan/50 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
>
{c}
</button>
))}
</div>
</div>
</section>
{/* Node table */}
<section className="container mx-auto max-w-6xl px-4">
<div className="overflow-hidden rounded-xl border border-zinc-800">
<table className="w-full text-sm">
<thead className="border-b border-zinc-800 bg-black/60">
<tr>
{["Node", "Category", "Onion", "Status", "Uptime", "Last seen", "Guard"].map((h) => (
<th key={h} className="px-4 py-3 text-left text-[10px] uppercase tracking-wider text-foreground/40">{h}</th>
))}
</tr>
</thead>
<tbody>
{filtered.map((node, i) => (
<tr key={node.id} className={`border-b border-zinc-800/50 ${i % 2 === 0 ? "bg-black/20" : "bg-black/40"} hover:bg-white/5 transition`}>
<td className="px-4 py-3 font-semibold text-foreground">{node.name}</td>
<td className="px-4 py-3">
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">{node.category}</span>
</td>
<td className="px-4 py-3 font-mono text-xs text-foreground/50">{node.onion}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-bold ${STATUS_COLORS[node.status]}`}>
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_DOT[node.status]}`} />
{node.status}
</span>
</td>
<td className="px-4 py-3 font-mono text-xs text-foreground/70">{node.uptime}</td>
<td className="px-4 py-3 text-xs text-foreground/40">{node.last}</td>
<td className="px-4 py-3 text-center">{node.guard ? <span className="text-neon-cyan text-base"></span> : <span className="text-foreground/20"></span>}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Submit */}
<section className="container mx-auto max-w-6xl px-4 mt-12">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="font-orbitron text-lg font-bold">Submit your node</h2>
<p className="text-sm text-foreground/60 mt-1">Add your hidden service to the Nexus monitoring ring. Requires a signed verification message.</p>
</div>
<button className="shrink-0 rounded-lg border border-neon-cyan/40 bg-neon-cyan/10 px-5 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20 transition">
Submit Node
</button>
</div>
</section>
</main>
</>
);
}

134
app/oracle/page.tsx Normal file
View File

@@ -0,0 +1,134 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
const LISTINGS = [
{ id: "1", title: "Verified exit scam early warning — active market", category: "Market Intel", price: 8, verified: true, age: "2h ago", rep: 94 },
{ id: "2", title: "Opsec hole in popular forum software (CVE-pending)", category: "Vulnerability", price: 25, verified: true, age: "6h ago", rep: 88 },
{ id: "3", title: "Darknet exchange front-running scheme — evidence package", category: "Fraud Intel", price: 15, verified: false, age: "1d ago", rep: 72 },
{ id: "4", title: "Law enforcement infrastructure node IPs (EU)", category: "Counter-Intel", price: 40, verified: true, age: "2d ago", rep: 91 },
{ id: "5", title: "Compromised vendor PGP key — do not transact", category: "Crypto", price: 5, verified: true, age: "3h ago", rep: 99 },
{ id: "6", title: "Tor relay operator correlation attack proof-of-concept", category: "Network", price: 30, verified: false, age: "5d ago", rep: 65 },
];
const CATS = ["All", "Market Intel", "Vulnerability", "Fraud Intel", "Counter-Intel", "Crypto", "Network"];
export default function OraclePage() {
const [cat, setCat] = useState("All");
const filtered = cat === "All" ? LISTINGS : LISTINGS.filter((l) => l.category === cat);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">
the oracle · verified intelligence market
</p>
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
THE <span className="text-neon-purple">ORACLE</span>
</h1>
<p className="mt-3 max-w-xl text-foreground/60">
Buy and sell verified darknet intelligence. Listings are staked submitters put VOID credits at risk. Community verification ensures accuracy.
</p>
</div>
<div className="flex gap-3">
<Link href="/sign-in" className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-2 text-sm font-bold text-neon-purple hover:bg-neon-purple/20">
Submit intel
</Link>
</div>
</div>
</section>
{/* Stats bar */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-6 rounded-xl border border-zinc-800 bg-black/40 p-4 text-sm">
{[
{ label: "Active listings", value: LISTINGS.length },
{ label: "Verified", value: LISTINGS.filter((l) => l.verified).length },
{ label: "Avg price", value: `${Math.round(LISTINGS.reduce((a, l) => a + l.price, 0) / LISTINGS.length)} VOID` },
{ label: "Avg rep score", value: `${Math.round(LISTINGS.reduce((a, l) => a + l.rep, 0) / LISTINGS.length)}%` },
].map((s) => (
<div key={s.label}>
<p className="text-xs text-foreground/40 uppercase tracking-wider">{s.label}</p>
<p className="font-bold text-foreground">{s.value}</p>
</div>
))}
</div>
</section>
{/* Category filter */}
<section className="container mx-auto max-w-6xl px-4 mb-6">
<div className="flex flex-wrap gap-2">
{CATS.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-4 py-1.5 text-xs font-semibold border transition ${
cat === c
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
}`}
>
{c}
</button>
))}
</div>
</section>
{/* Listings */}
<section className="container mx-auto max-w-6xl px-4">
<div className="space-y-3">
{filtered.map((listing) => (
<div
key={listing.id}
className="flex flex-col gap-3 rounded-xl border border-zinc-700/40 bg-black/40 p-5 sm:flex-row sm:items-center hover:border-zinc-500 transition cursor-pointer"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">
{listing.category}
</span>
{listing.verified && (
<span className="rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-2 py-0.5 text-[9px] uppercase tracking-wider text-neon-cyan">
verified
</span>
)}
<span className="text-[10px] text-foreground/30">{listing.age}</span>
</div>
<p className="font-semibold text-foreground text-sm leading-snug">{listing.title}</p>
<div className="mt-1 flex items-center gap-3">
<span className="text-xs text-foreground/40">Rep: <span className={listing.rep >= 85 ? "text-neon-green" : listing.rep >= 65 ? "text-yellow-500" : "text-red-400"}>{listing.rep}%</span></span>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="font-mono text-base font-bold text-neon-purple"> {listing.price}</span>
<button className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-1.5 text-xs font-bold text-neon-purple hover:bg-neon-purple/20 transition">
Purchase
</button>
</div>
</div>
))}
</div>
</section>
{/* Submit box */}
<section className="container mx-auto max-w-3xl px-4 mt-16">
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-8 text-center">
<h2 className="font-orbitron text-xl font-bold mb-2">Have actionable intelligence?</h2>
<p className="text-sm text-foreground/60 mb-5">Submit a listing. Stake VOID credits to guarantee accuracy payouts on verified buys.</p>
<Link href="/sign-in" className="inline-block rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
Submit Intelligence
</Link>
</div>
</section>
</main>
</>
);
}

View File

@@ -1,66 +1,19 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react";
import SecretLayer from "@/components/SecretLayer"; import SecretLayer from "@/components/SecretLayer";
import DDoSProtection from "@/components/DDoSProtection";
import OnionDisclosureBar from "@/components/OnionDisclosureBar"; import OnionDisclosureBar from "@/components/OnionDisclosureBar";
import HubChatterPublicStrip from "@/components/HubChatterPublicStrip"; import HubChatterPublicStrip from "@/components/HubChatterPublicStrip";
import AppProviders from "@/components/AppProviders"; import AppProviders from "@/components/AppProviders";
import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter"; import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter";
import RaffleCountdownBanner from "@/components/RaffleCountdownBanner";
const DDOS_KEY = "ddos_verified";
function readDdosVerified(): boolean {
if (typeof window === "undefined") return false;
try {
if (sessionStorage.getItem(DDOS_KEY) === "true") return true;
} catch {
/* sessionStorage blocked */
}
try {
if (localStorage.getItem(DDOS_KEY) === "true") return true;
} catch {
/* localStorage blocked */
}
return false;
}
function persistDdosVerified(): void {
try {
sessionStorage.setItem(DDOS_KEY, "true");
} catch {
/* ignore */
}
try {
localStorage.setItem(DDOS_KEY, "true");
} catch {
/* ignore */
}
}
export default function RootLayoutClient({ children }: { children: React.ReactNode }) { export default function RootLayoutClient({ children }: { children: React.ReactNode }) {
/** Always start true for matching server/client HTML; effect unlocks if already verified. */
const [isProtected, setIsProtected] = useState(true);
useEffect(() => {
if (readDdosVerified()) {
setIsProtected(false);
}
}, []);
const handleComplete = useCallback(() => {
persistDdosVerified();
setIsProtected(false);
}, []);
return ( return (
<>
{isProtected ? (
<DDoSProtection onComplete={handleComplete} />
) : (
<> <>
<AppProviders> <AppProviders>
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
{/* Raffle countdown sticky bar — visible on every page */}
<RaffleCountdownBanner />
<div className="flex-1">{children}</div> <div className="flex-1">{children}</div>
<DarkWebLaunchFooter /> <DarkWebLaunchFooter />
</div> </div>
@@ -70,7 +23,5 @@ export default function RootLayoutClient({ children }: { children: React.ReactNo
<SecretLayer /> <SecretLayer />
<OnionDisclosureBar /> <OnionDisclosureBar />
</> </>
)}
</>
); );
} }

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, Suspense, useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext"; import { useAccount } from "@/contexts/AccountContext";
export default function SignInPage() { function SignInContent() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { user, hydrated, signIn } = useAccount(); const { user, hydrated, signIn } = useAccount();
@@ -113,3 +113,17 @@ export default function SignInPage() {
</div> </div>
); );
} }
export default function SignInPage() {
return (
<Suspense
fallback={
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
Loading
</div>
}
>
<SignInContent />
</Suspense>
);
}

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, Suspense, useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext"; import { useAccount } from "@/contexts/AccountContext";
export default function SignUpPage() { function SignUpContent() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { user, hydrated, signUp } = useAccount(); const { user, hydrated, signUp } = useAccount();
@@ -127,3 +127,17 @@ export default function SignUpPage() {
</div> </div>
); );
} }
export default function SignUpPage() {
return (
<Suspense
fallback={
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
Loading
</div>
}
>
<SignUpContent />
</Suspense>
);
}

334
app/tools/page.tsx Normal file
View File

@@ -0,0 +1,334 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
import { useAccount } from "@/contexts/AccountContext";
import { TOOLS_CATALOG, CATEGORY_LABELS, type Tool, type ToolCategory } from "@/lib/toolsCatalog";
type ServerBalance = {
voidCredits: number;
unlockedToolIds: string[];
};
const CATEGORY_ORDER: ToolCategory[] = ["opsec", "crypto", "network", "intel", "comms", "identity"];
const CATEGORY_ICONS: Record<ToolCategory, string> = {
opsec: "🛡️",
crypto: "🔐",
network: "🌐",
intel: "🔍",
comms: "📡",
identity: "🪪",
};
function VoidBadge({ credits }: { credits: number }) {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-neon-purple/40 bg-neon-purple/10 px-3 py-1 font-mono text-sm text-neon-purple">
{credits.toLocaleString()} VOID
</span>
);
}
function ToolCard({
tool,
unlocked,
voidBalance,
onUnlock,
unlocking,
}: {
tool: Tool;
unlocked: boolean;
voidBalance: number;
onUnlock: (tool: Tool) => void;
unlocking: boolean;
}) {
const canAfford = voidBalance >= tool.cost;
return (
<div
className={`relative flex flex-col rounded-xl border p-5 transition-all duration-200 ${
unlocked
? "border-neon-cyan/40 bg-neon-cyan/5"
: tool.comingSoon
? "border-zinc-700/40 bg-black/30 opacity-60"
: canAfford
? "border-zinc-600/40 bg-black/40 hover:border-neon-purple/50"
: "border-zinc-700/40 bg-black/30"
}`}
>
{unlocked && (
<div className="absolute right-3 top-3 rounded-full bg-neon-cyan/15 px-2 py-0.5 text-[10px] font-bold tracking-widest text-neon-cyan">
UNLOCKED
</div>
)}
{tool.comingSoon && !unlocked && (
<div className="absolute right-3 top-3 rounded-full bg-zinc-700/50 px-2 py-0.5 text-[10px] font-bold tracking-widest text-zinc-400">
SOON
</div>
)}
<div className="mb-3 flex items-start gap-3">
<span className="text-3xl">{tool.icon}</span>
<div className="min-w-0">
<p className="font-mono text-[10px] uppercase tracking-widest text-neon-purple/60">
{CATEGORY_ICONS[tool.category]} {CATEGORY_LABELS[tool.category]}
</p>
<h3 className="font-orbitron mt-0.5 text-base font-bold text-foreground">{tool.name}</h3>
<p className="mt-0.5 text-xs text-foreground/50">{tool.tagline}</p>
</div>
</div>
<p className="mb-4 flex-1 text-sm text-foreground/70 leading-relaxed">{tool.description}</p>
<ul className="mb-5 space-y-1">
{tool.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-xs text-foreground/55">
<span className="text-neon-cyan/70"></span> {f}
</li>
))}
</ul>
<div className="mt-auto flex items-center justify-between gap-3">
<span className="font-mono text-sm font-bold text-neon-purple">
{tool.cost} VOID
</span>
{unlocked ? (
<Link
href={`/tools/${tool.id}`}
className="rounded-lg bg-neon-cyan/15 px-4 py-2 text-xs font-bold text-neon-cyan transition hover:bg-neon-cyan/25"
>
Open
</Link>
) : tool.comingSoon ? (
<button disabled className="cursor-not-allowed rounded-lg bg-zinc-700/40 px-4 py-2 text-xs text-zinc-500">
Coming soon
</button>
) : (
<button
onClick={() => onUnlock(tool)}
disabled={!canAfford || unlocking}
className={`rounded-lg px-4 py-2 text-xs font-bold transition ${
canAfford
? "bg-neon-purple/20 text-neon-purple hover:bg-neon-purple/35 border border-neon-purple/30"
: "cursor-not-allowed bg-zinc-800/50 text-zinc-500 border border-zinc-700/30"
}`}
>
{unlocking ? "Unlocking…" : canAfford ? "Unlock" : "Need more VOID"}
</button>
)}
</div>
</div>
);
}
export default function ToolsPage() {
const { user, hydrated } = useAccount();
const [balance, setBalance] = useState<ServerBalance | null>(null);
const [loading, setLoading] = useState(false);
const [unlocking, setUnlocking] = useState<string | null>(null);
const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null);
const [filter, setFilter] = useState<ToolCategory | "all">("all");
const fetchBalance = useCallback(async () => {
if (!user?.username) return;
setLoading(true);
try {
const res = await fetch(`/api/credits/balance?handle=${encodeURIComponent(user.username)}`);
const data = (await res.json()) as { ok: boolean; voidCredits: number; unlockedToolIds: string[] };
if (data.ok) setBalance({ voidCredits: data.voidCredits, unlockedToolIds: data.unlockedToolIds });
} catch { /* ignore */ } finally {
setLoading(false);
}
}, [user?.username]);
useEffect(() => { void fetchBalance(); }, [fetchBalance]);
const showToast = (msg: string, ok: boolean) => {
setToast({ msg, ok });
setTimeout(() => setToast(null), 4000);
};
const handleUnlock = async (tool: Tool) => {
if (!user?.username || !balance) return;
setUnlocking(tool.id);
try {
const res = await fetch("/api/tools/unlock", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: user.username, toolId: tool.id }),
});
const data = (await res.json()) as { ok: boolean; error?: string; voidBalance?: number; voidSpent?: number };
if (data.ok) {
setBalance((prev) =>
prev
? {
voidCredits: data.voidBalance ?? prev.voidCredits - tool.cost,
unlockedToolIds: [...prev.unlockedToolIds, tool.id],
}
: prev,
);
showToast(`${tool.name} unlocked! ${tool.cost} VOID spent.`, true);
} else {
showToast(data.error ?? "Unlock failed", false);
}
} catch {
showToast("Network error", false);
} finally {
setUnlocking(null);
}
};
const filtered = filter === "all" ? TOOLS_CATALOG : TOOLS_CATALOG.filter((t) => t.category === filter);
if (!hydrated) return null;
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
{/* Header */}
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">
access-gated · void credits required
</p>
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
VOID<span className="text-neon-purple"> TOOLS</span>
</h1>
<p className="mt-3 max-w-xl text-foreground/60">
Premium privacy, OPSEC, and intelligence tools. Unlock permanently with VOID credits earned from BTC deposits. No subscriptions own it forever.
</p>
</div>
<div className="flex flex-col items-end gap-2">
{user ? (
<>
<VoidBadge credits={balance?.voidCredits ?? 0} />
{loading && <p className="text-xs text-foreground/40">Syncing</p>}
<Link href="/account/add-funds" className="text-xs text-neon-cyan/70 underline hover:text-neon-cyan">
+ Deposit BTC for VOID credits
</Link>
</>
) : (
<Link
href="/sign-in"
className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-5 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/20"
>
Sign in to unlock tools
</Link>
)}
</div>
</div>
</section>
{/* Category filter */}
<section className="container mx-auto max-w-6xl px-4 mb-8">
<div className="flex flex-wrap gap-2">
<button
onClick={() => setFilter("all")}
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition border ${
filter === "all"
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
}`}
>
All
</button>
{CATEGORY_ORDER.map((cat) => (
<button
key={cat}
onClick={() => setFilter(cat)}
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition border ${
filter === cat
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
}`}
>
{CATEGORY_ICONS[cat]} {CATEGORY_LABELS[cat]}
</button>
))}
</div>
</section>
{/* How VOID credits work */}
{!user && (
<section className="container mx-auto max-w-6xl px-4 mb-10">
<div className="rounded-xl border border-neon-purple/20 bg-neon-purple/5 p-6">
<h2 className="font-orbitron mb-3 text-lg font-bold text-neon-purple">How VOID Credits Work</h2>
<div className="grid gap-4 sm:grid-cols-3 text-sm text-foreground/70">
<div>
<p className="mb-1 font-bold text-foreground">1. Deposit BTC</p>
<p>Go to Add Funds. BTCPay generates a unique BTC address. Send any amount.</p>
</div>
<div>
<p className="mb-1 font-bold text-foreground">2. Receive VOID</p>
<p>After 1 confirmation, credits appear instantly. 1000 sats 1 VOID credit.</p>
</div>
<div>
<p className="mb-1 font-bold text-foreground">3. Unlock Forever</p>
<p>Spend VOID to unlock tools permanently. Credits are server-side work on any device.</p>
</div>
</div>
</div>
</section>
)}
{/* Tools grid */}
<section className="container mx-auto max-w-6xl px-4">
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
unlocked={balance?.unlockedToolIds.includes(tool.id) ?? false}
voidBalance={balance?.voidCredits ?? 0}
onUnlock={handleUnlock}
unlocking={unlocking === tool.id}
/>
))}
</div>
</section>
{/* Stats bar */}
<section className="container mx-auto max-w-6xl px-4 mt-16">
<div className="rounded-xl border border-zinc-800 bg-black/40 p-5 flex flex-wrap gap-6 text-sm">
<div>
<p className="text-foreground/40 text-xs font-mono mb-1">TOTAL TOOLS</p>
<p className="font-bold text-foreground">{TOOLS_CATALOG.filter((t) => !t.comingSoon).length} available</p>
</div>
<div>
<p className="text-foreground/40 text-xs font-mono mb-1">YOUR UNLOCKED</p>
<p className="font-bold text-neon-cyan">{balance?.unlockedToolIds.length ?? 0}</p>
</div>
<div>
<p className="text-foreground/40 text-xs font-mono mb-1">VOID BALANCE</p>
<p className="font-bold text-neon-purple"> {(balance?.voidCredits ?? 0).toLocaleString()}</p>
</div>
<div className="ml-auto flex items-center">
<Link
href="/account/add-funds"
className="rounded-lg bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-2.5 text-sm font-bold text-background hover:opacity-90"
>
+ Add VOID Credits
</Link>
</div>
</div>
</section>
</main>
{/* Toast */}
{toast && (
<div
className={`fixed bottom-6 right-6 z-50 rounded-xl border px-5 py-3 text-sm font-semibold shadow-xl ${
toast.ok
? "border-neon-cyan/40 bg-black/90 text-neon-cyan"
: "border-red-500/40 bg-black/90 text-red-400"
}`}
>
{toast.msg}
</div>
)}
</>
);
}

View File

@@ -1,73 +1,206 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { useAccount } from "@/contexts/AccountContext"; import { useAccount } from "@/contexts/AccountContext";
// ─── Types ───────────────────────────────────────────────────────────────────
type Msg = { id: string; from: string; text: string; ts: number; channel?: string }; type Msg = { id: string; from: string; text: string; ts: number; channel?: string };
type HandleRecord = {
voidCredits: number;
claimedInvoices: Record<string, number>;
unlockedTools: { toolId: string; unlockedAt: number; voidSpent: number }[];
};
type LedgerResponse = { ok: boolean; handles: Record<string, HandleRecord>; invoiceIndex: Record<string, string> };
type RaffleStats = { ok: boolean; drawAt: number; totalTickets: number; participants: number };
type RaffleEntry = { handle: string; tickets: number; boughtAt: number };
// ─── Helpers ─────────────────────────────────────────────────────────────────
function fmt(ms: number) {
if (ms <= 0) return "READY TO DRAW";
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (d > 0) return `${d}d ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
}
function StatCard({ label, value, sub, accent }: { label: string; value: string | number; sub?: string; accent?: string }) {
return (
<div className="rounded-lg border border-[#00ff66]/20 bg-black/50 p-4">
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">{label}</div>
<div className={`text-2xl font-black font-mono tabular-nums ${accent ?? "text-[#00ff66]"}`}>{value}</div>
{sub && <div className="text-[10px] text-zinc-600 mt-1">{sub}</div>}
</div>
);
}
type Tab = "overview" | "credits" | "raffle" | "comms" | "logs";
// ─── Main ─────────────────────────────────────────────────────────────────────
export default function VaultAdminDashboard() { export default function VaultAdminDashboard() {
const { user, hydrated, signIn } = useAccount(); const { user, hydrated, signIn } = useAccount();
const [btcUsd, setBtcUsd] = useState<number | null>(null); const [tab, setTab] = useState<Tab>("overview");
const [messages, setMessages] = useState<Msg[]>([]);
const [broadcast, setBroadcast] = useState("");
const [terminalLines, setTerminalLines] = useState<string[]>([]);
const [loginPass, setLoginPass] = useState(""); const [loginPass, setLoginPass] = useState("");
const [loginError, setLoginError] = useState(""); const [loginError, setLoginError] = useState("");
// BTC
const [btcUsd, setBtcUsd] = useState<number | null>(null);
// Ledger
const [handles, setHandles] = useState<Record<string, HandleRecord>>({});
const [invoiceIndex, setInvoiceIndex] = useState<Record<string, string>>({});
// Raffle
const [raffleStats, setRaffleStats] = useState<RaffleStats | null>(null);
const [raffleEntries, setRaffleEntries] = useState<RaffleEntry[]>([]);
const [raffleRemaining, setRaffleRemaining] = useState(0);
const [drawResult, setDrawResult] = useState<string | null>(null);
// Credit ops
const [creditHandle, setCreditHandle] = useState("");
const [creditAmt, setCreditAmt] = useState("");
const [creditMsg, setCreditMsg] = useState<{ text: string; ok: boolean } | null>(null);
// Comms
const [messages, setMessages] = useState<Msg[]>([]);
const [broadcast, setBroadcast] = useState("");
// Logs
const [terminalLines, setTerminalLines] = useState<string[]>([
`[BOOT] CyberLux admin shell initialised.`,
`[AUTH] drjones authenticated via local session.`,
]);
const logRef = useRef<HTMLDivElement>(null);
// ── Fetchers ────────────────────────────────────────────────────────────────
const fetchBtc = useCallback(async () => { const fetchBtc = useCallback(async () => {
try { try {
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { const r = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { cache: "no-store" });
cache: "no-store", const j = await r.json() as { bitcoin?: { usd?: number } };
}); setBtcUsd(j.bitcoin?.usd ?? null);
const j = await res.json(); } catch { /* ignore */ }
setBtcUsd(j.bitcoin?.usd || null); }, []);
} catch {
// ignore const fetchLedger = useCallback(async () => {
try {
const r = await fetch("/api/admin/ledger");
if (!r.ok) return;
const d = await r.json() as LedgerResponse;
if (d.ok) { setHandles(d.handles); setInvoiceIndex(d.invoiceIndex); }
} catch { /* ignore */ }
}, []);
const fetchRaffle = useCallback(async () => {
try {
const r = await fetch("/api/raffle/buy");
const d = await r.json() as RaffleStats;
if (d.ok) setRaffleStats(d);
const r2 = await fetch("/api/admin/raffle");
if (r2.ok) {
const d2 = await r2.json() as { ok: boolean; entries: RaffleEntry[] };
if (d2.ok) setRaffleEntries(d2.entries);
} }
} catch { /* ignore */ }
}, []); }, []);
const fetchMessages = useCallback(async () => { const fetchMessages = useCallback(async () => {
try { try {
const res = await fetch("/api/messages?channel=all"); const r = await fetch("/api/messages?channel=all");
const data = await res.json(); const d = await r.json() as { ok: boolean; messages: Msg[] };
if (data.ok) setMessages(data.messages); if (d.ok) setMessages(d.messages);
} catch { } catch { /* ignore */ }
// ignore
}
}, []); }, []);
const log = useCallback((line: string) => {
setTerminalLines((prev) => {
const next = [...prev.slice(-99), line];
return next;
});
setTimeout(() => { logRef.current?.scrollTo({ top: 9999, behavior: "smooth" }); }, 50);
}, []);
// ── Init ────────────────────────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!user || user.username !== "drjones") return;
void fetchBtc(); void fetchBtc();
void fetchLedger();
void fetchRaffle();
void fetchMessages(); void fetchMessages();
const id = setInterval(() => { const id = setInterval(() => { void fetchBtc(); void fetchLedger(); void fetchRaffle(); void fetchMessages(); }, 15_000);
void fetchBtc();
void fetchMessages();
}, 10_000);
return () => clearInterval(id); return () => clearInterval(id);
}, [fetchBtc, fetchMessages]); }, [user, fetchBtc, fetchLedger, fetchRaffle, fetchMessages]);
// Raffle countdown
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { if (!raffleStats) return;
const logs = [ const t = setInterval(() => setRaffleRemaining(raffleStats.drawAt - Date.now()), 1000);
`[TOR_ADMIN] Relay ping successful. Circuit latency: ${Math.floor(Math.random() * 80 + 20)}ms`, return () => clearInterval(t);
`[NGINX_ROUTER] Routing loopback 127.0.0.1:8080 -> next:3000`, }, [raffleStats]);
`[SECURITY] Null-routing unauthorized clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
`[BTC_NODE] Synced block height ${839000 + Math.floor(Math.random() * 1000)}`, // Simulated terminal feed
`[SYSTEM] Memory stable. Cache flushed.`, useEffect(() => {
if (!user || user.username !== "drjones") return;
const lines = [
() => `[TOR] Circuit ping ${Math.floor(Math.random() * 80 + 20)}ms — relay healthy`,
() => `[NGINX] 127.0.0.1:8080 → next:3000 — ${Math.floor(Math.random() * 8 + 1)} req/s`,
() => `[SHIELD] Null-routing clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.x.x`,
() => `[BTC] Block height ${895000 + Math.floor(Math.random() * 500)} confirmed`,
() => `[LEDGER] Balances consistent — ${Object.keys(handles).length} handles tracked`,
]; ];
setTerminalLines((prev) => [...prev.slice(-15), logs[Math.floor(Math.random() * logs.length)]!]); const t = setInterval(() => log(lines[Math.floor(Math.random() * lines.length)]!()), 4000);
}, 3500); return () => clearInterval(t);
return () => clearInterval(interval); }, [user, handles, log]);
}, []);
// ── Actions ──────────────────────────────────────────────────────────────────
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoginError(""); setLoginError("");
const res = await signIn("drjones", loginPass); const res = await signIn("drjones", loginPass);
if (!res.ok) { if (!res.ok) setLoginError(res.error ?? "Access Denied");
setLoginError(res.error || "Access Denied"); };
const adminCredit = async () => {
const amt = parseFloat(creditAmt);
if (!creditHandle.trim() || isNaN(amt) || amt <= 0) {
setCreditMsg({ text: "Handle and positive amount required", ok: false });
return;
} }
setCreditMsg(null);
try {
const r = await fetch("/api/admin/credit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: creditHandle.trim(), amount: Math.floor(amt) }),
});
const d = await r.json() as { ok: boolean; newBalance?: number; error?: string };
if (d.ok) {
setCreditMsg({ text: `${Math.floor(amt)} VOID → @${creditHandle.trim()} (new balance: ${d.newBalance})`, ok: true });
log(`[ADMIN] Manual credit: +${Math.floor(amt)} VOID → @${creditHandle.trim()}`);
void fetchLedger();
} else {
setCreditMsg({ text: d.error ?? "Failed", ok: false });
}
} catch { setCreditMsg({ text: "Network error", ok: false }); }
};
const doDraw = () => {
if (raffleEntries.length === 0) { setDrawResult("No entries yet."); return; }
const pool: string[] = [];
for (const e of raffleEntries) {
for (let i = 0; i < e.tickets; i++) pool.push(e.handle);
}
const winner = pool[Math.floor(Math.random() * pool.length)]!;
setDrawResult(`🎉 WINNER: @${winner} (${raffleEntries.find((e) => e.handle === winner)?.tickets ?? 1} tickets)`);
log(`[RAFFLE] Manual draw executed — winner: @${winner}`);
}; };
const handleBroadcast = async () => { const handleBroadcast = async () => {
@@ -76,168 +209,449 @@ export default function VaultAdminDashboard() {
await fetch("/api/messages", { await fetch("/api/messages", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: "System Admin", text: `[GLOBAL BROADCAST] ${broadcast}`, channel: "global" }), body: JSON.stringify({ from: "System Admin", text: `[BROADCAST] ${broadcast}`, channel: "global" }),
}); });
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST SENT: ${broadcast}`]); log(`[COMMS] Global broadcast sent: "${broadcast}"`);
setBroadcast(""); setBroadcast("");
void fetchMessages(); void fetchMessages();
} catch { } catch { log("[COMMS] Broadcast failed!"); }
setTerminalLines((prev) => [...prev, `[ADMIN] Broadcast Failed!`]);
}
}; };
const deleteMessage = async (id: string) => { const deleteMessage = async (id: string) => {
await fetch(`/api/messages?id=${id}`, { method: "DELETE" }); await fetch(`/api/messages?id=${id}`, { method: "DELETE" });
log(`[COMMS] Deleted message ${id}`);
void fetchMessages(); void fetchMessages();
setTerminalLines((prev) => [...prev, `[ADMIN] Deleted message ${id}`]);
}; };
const clearAllMessages = async () => { const nukeMessages = async () => {
if (!confirm("Are you sure you want to nuke all global communications?")) return; if (!confirm("Nuke all comms?")) return;
await fetch(`/api/messages?all=true`, { method: "DELETE" }); await fetch(`/api/messages?all=true`, { method: "DELETE" });
log("[COMMS] All messages purged.");
void fetchMessages(); void fetchMessages();
setTerminalLines((prev) => [...prev, `[ADMIN] Nuked all global messages.`]);
}; };
if (!hydrated) { // ── Auth gate ────────────────────────────────────────────────────────────────
return <div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono">Initializing connection...</div>;
}
// Only drjones gets to see the true dashboard if (!hydrated) return (
if (!user || user.username !== "drjones") { <div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono text-sm">
return ( Initializing
</div>
);
if (!user || user.username !== "drjones") return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050000] p-6 font-mono text-[#00ff66]"> <div className="flex min-h-screen flex-col items-center justify-center bg-[#050000] p-6 font-mono text-[#00ff66]">
<div className="w-full max-w-md border border-[#00ff66]/30 bg-black/80 p-8 shadow-[0_0_30px_rgba(0,255,102,0.15)] rounded-lg text-center"> <div className="w-full max-w-sm rounded-xl border border-[#00ff66]/25 bg-black/80 p-8 shadow-[0_0_40px_rgba(0,255,102,0.12)]">
<h1 className="mb-2 text-2xl font-black uppercase text-red-500 tracking-widest">RESTRICTED ZONE</h1> <div className="mb-6 text-center">
<p className="mb-8 text-xs text-foreground/60">EldritchWeave Tor Network Admin</p> <div className="text-[10px] uppercase tracking-[0.4em] text-[#00ff66]/40 mb-2">CYBERLUX NODE</div>
<h1 className="text-2xl font-black uppercase tracking-widest text-red-500">RESTRICTED</h1>
<form onSubmit={handleLogin} className="flex flex-col gap-4 text-left"> <p className="mt-2 text-[10px] text-zinc-600">Operator access only</p>
<div> </div>
<label className="text-xs uppercase text-[#00ff66]/70">Admin Passphrase</label> <form onSubmit={handleLogin} className="flex flex-col gap-4">
<input <input
type="password" type="password"
value={loginPass} value={loginPass}
onChange={(e) => setLoginPass(e.target.value)} onChange={(e) => setLoginPass(e.target.value)}
className="mt-1 w-full rounded border border-[#00ff66]/30 bg-black px-4 py-2 text-[#00ff66] focus:border-[#00ff66] focus:outline-none" className="w-full rounded border border-[#00ff66]/25 bg-black px-4 py-3 text-[#00ff66] text-sm placeholder:text-zinc-700 focus:border-[#00ff66]/60 focus:outline-none"
placeholder="Enter passphrase..." placeholder="passphrase"
autoFocus
/> />
</div> {loginError && <div className="text-xs text-red-500">{loginError}</div>}
{loginError && <div className="text-xs font-bold text-red-500">{loginError}</div>} <button type="submit" className="w-full rounded bg-[#00ff66]/15 py-3 text-xs font-bold uppercase tracking-widest text-[#00ff66] hover:bg-[#00ff66]/25 transition-colors">
<button type="submit" className="mt-2 w-full bg-[#00ff66]/20 py-3 font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/30 transition-colors">
AUTHORIZE AUTHORIZE
</button> </button>
</form> </form>
<Link href="/" className="mt-6 block text-center text-[10px] uppercase text-zinc-700 hover:text-zinc-500 transition-colors">
<Link href="/" className="mt-8 block text-[10px] uppercase text-[#00ff66]/40 hover:text-[#00ff66] underline"> Return to hub
Return to Hub
</Link> </Link>
</div> </div>
</div> </div>
); );
}
// ── Computed stats ───────────────────────────────────────────────────────────
const allHandles = Object.entries(handles);
const totalVoidCirculating = allHandles.reduce((s, [, r]) => s + r.voidCredits, 0);
const totalInvoices = Object.keys(invoiceIndex).length;
const totalToolUnlocks = allHandles.reduce((s, [, r]) => s + r.unlockedTools.length, 0);
const TABS: { id: Tab; label: string; icon: string }[] = [
{ id: "overview", label: "Overview", icon: "◈" },
{ id: "credits", label: "Credit Ops", icon: "◆" },
{ id: "raffle", label: "Raffle", icon: "🎟" },
{ id: "comms", label: "Comms", icon: "📡" },
{ id: "logs", label: "System Log", icon: "▸" },
];
// ── Render ────────────────────────────────────────────────────────────────────
return ( return (
<div className="flex min-h-screen flex-col overflow-hidden bg-[#030008] p-6 font-mono text-[#00ff66]"> <div className="min-h-screen bg-[#030008] font-mono text-[#00ff66]">
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-[#00ff66]/30 pb-4"> {/* Header */}
<div className="flex items-center gap-4"> <header className="border-b border-[#00ff66]/20 bg-black/60 backdrop-blur px-6 py-3 flex items-center justify-between gap-4 sticky top-0 z-40">
<div className="h-3 w-3 animate-ping rounded-full bg-red-600 shadow-[0_0_10px_red]" /> <div className="flex items-center gap-3">
<h1 className="text-2xl font-black tracking-tighter text-white uppercase text-shadow-sm shadow-red-500">Tor Admin Dashboard</h1> <span className="inline-block h-2 w-2 rounded-full bg-[#00ff66] shadow-[0_0_8px_#00ff66] animate-pulse" />
<span className="text-sm font-black uppercase tracking-widest text-white">CYBERLUX COMMAND</span>
<span className="hidden sm:inline text-[10px] text-zinc-600 uppercase tracking-widest">// OPERATOR CONSOLE</span>
</div> </div>
<div className="flex flex-wrap gap-6 text-[10px] uppercase tracking-widest opacity-80"> <div className="flex items-center gap-4 text-[10px] uppercase text-zinc-500">
<div>Admin: <span className="text-red-400">Dr. Jones</span></div> <span>@drjones</span>
<div>Status: <span className="text-[#00ff66]">GOD MODE</span></div> <span className="text-[#00ff66]">GOD MODE</span>
{btcUsd && <span className="text-yellow-500">BTC ${btcUsd.toLocaleString()}</span>}
<Link href="/" className="rounded border border-zinc-800 px-2 py-1 hover:border-zinc-600 transition-colors"> Hub</Link>
</div> </div>
</header> </header>
<div className="grid flex-1 grid-cols-12 gap-6"> {/* Tab bar */}
<div className="col-span-12 space-y-6 lg:col-span-4"> <nav className="border-b border-[#00ff66]/10 bg-black/40 px-6 flex gap-1">
<div className="rounded-xl border border-[#00ff66]/30 bg-black/60 p-6 shadow-[0_0_20px_rgba(0,255,102,0.1)]"> {TABS.map((t) => (
<h2 className="mb-4 text-xs font-bold uppercase text-[#00ff66]">System Telemetry</h2> <button
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2"> key={t.id}
<span className="text-[10px] uppercase">Active Relays</span> onClick={() => setTab(t.id)}
<span className="text-lg font-bold text-white">43</span> className={`px-4 py-3 text-xs font-bold uppercase tracking-wider transition-colors border-b-2 ${
tab === t.id
? "border-[#00ff66] text-[#00ff66]"
: "border-transparent text-zinc-600 hover:text-zinc-400"
}`}
>
{t.icon} {t.label}
</button>
))}
</nav>
{/* Body */}
<main className="p-6 max-w-7xl mx-auto">
{/* ── OVERVIEW ─────────────────────────────────────────────────────── */}
{tab === "overview" && (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
<StatCard label="VOID Circulating" value={totalVoidCirculating} sub="across all handles" />
<StatCard label="Handles" value={allHandles.length} sub="registered" />
<StatCard label="Invoices" value={totalInvoices} sub="total created" />
<StatCard label="Tool Unlocks" value={totalToolUnlocks} sub="all time" />
<StatCard label="BTC / USD" value={btcUsd ? `$${btcUsd.toLocaleString()}` : "…"} sub="live oracle" accent="text-yellow-400" />
</div> </div>
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
<span className="text-[10px] uppercase">BTC/USD Oracle</span> {/* Raffle snapshot */}
<span className="text-lg font-bold text-white">${btcUsd ? btcUsd.toLocaleString() : "..."}</span> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<StatCard label="Raffle Tickets" value={raffleStats?.totalTickets ?? 0} sub="this round" accent="text-purple-400" />
<StatCard label="Participants" value={raffleStats?.participants ?? 0} sub="this round" accent="text-purple-400" />
<div className="rounded-lg border border-purple-500/20 bg-black/50 p-4">
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">Next Draw</div>
<div className="text-2xl font-black font-mono tabular-nums text-purple-400">
{raffleStats ? fmt(raffleStats.drawAt - Date.now()) : "…"}
</div> </div>
<div className="flex items-end justify-between">
<span className="text-[10px] uppercase">Firewall Integrity</span>
<span className="text-lg font-bold text-[#00ff66]">100%</span>
</div> </div>
</div> </div>
<div className="rounded-xl border border-red-600/50 bg-[#1a0000]/60 p-6 shadow-[0_0_20px_rgba(255,0,0,0.15)]"> {/* Top handles by VOID */}
<h2 className="mb-4 text-xs font-black uppercase text-red-500">Global Network Override</h2> <div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Top VOID Holders</h2>
{allHandles.length === 0 ? (
<div className="text-xs text-zinc-600">No handles registered yet.</div>
) : (
<div className="space-y-2">
{[...allHandles].sort((a, b) => b[1].voidCredits - a[1].voidCredits).slice(0, 10).map(([handle, rec]) => (
<div key={handle} className="flex items-center gap-3">
<span className="text-xs text-zinc-400 w-32 truncate">@{handle}</span>
<div className="flex-1 h-2 rounded-full bg-zinc-900">
<div
className="h-2 rounded-full bg-[#00ff66]/60"
style={{ width: `${Math.min(100, (rec.voidCredits / Math.max(1, totalVoidCirculating)) * 100)}%` }}
/>
</div>
<span className="text-xs font-bold text-[#00ff66] w-16 text-right tabular-nums">{rec.voidCredits} V</span>
<span className="text-[10px] text-zinc-600 w-20 text-right">{rec.unlockedTools.length} tools</span>
</div>
))}
</div>
)}
</div>
{/* Quick ops */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ label: "VOID Tools", href: "/tools", desc: "Tool marketplace", icon: "🔓" },
{ label: "DarkHost", href: "/hosting", desc: "Hosting panel", icon: "🖥️" },
{ label: "Add Funds", href: "/account/add-funds", desc: "BTC deposit", icon: "₿" },
].map((l) => (
<Link key={l.href} href={l.href} className="flex items-center gap-3 rounded-lg border border-zinc-800 bg-black/30 p-4 hover:border-[#00ff66]/40 transition-colors">
<span className="text-xl">{l.icon}</span>
<div>
<div className="text-xs font-bold text-white">{l.label}</div>
<div className="text-[10px] text-zinc-600">{l.desc}</div>
</div>
</Link>
))}
</div>
</div>
)}
{/* ── CREDIT OPS ───────────────────────────────────────────────────── */}
{tab === "credits" && (
<div className="space-y-6">
{/* Manual credit */}
<div className="rounded-xl border border-[#00ff66]/25 bg-black/50 p-6">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Manual VOID Credit</h2>
<div className="flex flex-wrap gap-3 items-end">
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase text-zinc-500">Handle</label>
<input
value={creditHandle}
onChange={(e) => setCreditHandle(e.target.value)}
placeholder="username"
className="rounded border border-zinc-800 bg-black px-3 py-2 text-sm text-[#00ff66] placeholder:text-zinc-700 focus:border-[#00ff66]/50 focus:outline-none w-44"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase text-zinc-500">VOID Amount</label>
<input
value={creditAmt}
onChange={(e) => setCreditAmt(e.target.value)}
placeholder="50"
type="number"
min="1"
className="rounded border border-zinc-800 bg-black px-3 py-2 text-sm text-[#00ff66] placeholder:text-zinc-700 focus:border-[#00ff66]/50 focus:outline-none w-32"
/>
</div>
<button
onClick={adminCredit}
className="rounded bg-[#00ff66]/15 border border-[#00ff66]/30 px-5 py-2 text-xs font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/25 transition-colors"
>
CREDIT
</button>
</div>
{creditMsg && (
<div className={`mt-3 text-xs ${creditMsg.ok ? "text-[#00ff66]" : "text-red-400"}`}>
{creditMsg.text}
</div>
)}
</div>
{/* Full ledger table */}
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">
Full VOID Ledger {allHandles.length} handles
</h2>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[10px] uppercase text-zinc-600 border-b border-zinc-800">
<th className="text-left py-2 pr-4">Handle</th>
<th className="text-right py-2 pr-4">VOID</th>
<th className="text-right py-2 pr-4">Invoices</th>
<th className="text-right py-2 pr-4">Tools</th>
<th className="text-right py-2">Spent (VOID)</th>
</tr>
</thead>
<tbody>
{allHandles.length === 0 ? (
<tr><td colSpan={5} className="py-6 text-center text-zinc-600">No handles yet.</td></tr>
) : (
[...allHandles]
.sort((a, b) => b[1].voidCredits - a[1].voidCredits)
.map(([handle, rec]) => {
const spent = rec.unlockedTools.reduce((s, t) => s + t.voidSpent, 0);
return (
<tr key={handle} className="border-b border-zinc-900 hover:bg-white/2 transition-colors">
<td className="py-2 pr-4 text-white font-bold">@{handle}</td>
<td className="py-2 pr-4 text-right tabular-nums text-[#00ff66] font-bold">{rec.voidCredits}</td>
<td className="py-2 pr-4 text-right tabular-nums text-zinc-400">{Object.keys(rec.claimedInvoices).length}</td>
<td className="py-2 pr-4 text-right tabular-nums text-purple-400">{rec.unlockedTools.length}</td>
<td className="py-2 text-right tabular-nums text-zinc-500">{spent}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Invoice index */}
<div className="rounded-xl border border-zinc-800 bg-black/40 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-zinc-500">
Invoice Index {totalInvoices} total
</h2>
<div className="overflow-x-auto max-h-48 overflow-y-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="text-zinc-700 border-b border-zinc-900 uppercase">
<th className="text-left py-1 pr-4">Invoice ID</th>
<th className="text-left py-1">Handle</th>
</tr>
</thead>
<tbody>
{Object.entries(invoiceIndex).slice(-30).reverse().map(([inv, handle]) => (
<tr key={inv} className="border-b border-zinc-900/60">
<td className="py-1 pr-4 font-mono text-zinc-500">{inv.slice(0, 20)}</td>
<td className="py-1 text-zinc-400">@{handle}</td>
</tr>
))}
{totalInvoices === 0 && (
<tr><td colSpan={2} className="py-4 text-center text-zinc-700">No invoices yet.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* ── RAFFLE ───────────────────────────────────────────────────────── */}
{tab === "raffle" && (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard label="Tickets Sold" value={raffleStats?.totalTickets ?? 0} accent="text-purple-400" />
<StatCard label="Participants" value={raffleStats?.participants ?? 0} accent="text-purple-400" />
<div className="rounded-lg border border-purple-500/20 bg-black/50 p-4 col-span-2">
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">Time to Draw</div>
<div className="text-3xl font-black font-mono tabular-nums text-purple-400">{fmt(raffleRemaining)}</div>
</div>
</div>
{/* Draw control */}
<div className="rounded-xl border border-purple-500/30 bg-[#0d0010]/60 p-6">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-purple-400">Manual Draw</h2>
<p className="text-xs text-zinc-500 mb-4">
Executes a weighted random draw client-side for preview purposes. Winner is selected proportional to ticket count.
</p>
<button
onClick={doDraw}
className="rounded bg-purple-900/40 border border-purple-500/40 px-6 py-2 text-sm font-bold text-purple-300 hover:bg-purple-900/60 transition-colors"
>
🎲 EXECUTE DRAW
</button>
{drawResult && (
<div className="mt-4 rounded-lg border border-purple-500/30 bg-purple-900/20 p-4 text-sm font-bold text-purple-300">
{drawResult}
</div>
)}
</div>
{/* Entries */}
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Current Entries</h2>
{raffleEntries.length === 0 ? (
<div className="text-xs text-zinc-600 py-4">No entries yet.</div>
) : (
<div className="space-y-2">
{[...raffleEntries].sort((a, b) => b.tickets - a.tickets).map((e) => {
const pct = raffleStats ? Math.round((e.tickets / raffleStats.totalTickets) * 100) : 0;
return (
<div key={e.handle} className="flex items-center gap-3">
<span className="text-xs text-zinc-400 w-32 truncate">@{e.handle}</span>
<div className="flex-1 h-2 rounded-full bg-zinc-900">
<div className="h-2 rounded-full bg-purple-500/60" style={{ width: `${pct}%` }} />
</div>
<span className="text-xs font-bold text-purple-300 w-16 text-right tabular-nums">{e.tickets} tickets</span>
<span className="text-[10px] text-zinc-600 w-10 text-right">{pct}%</span>
</div>
);
})}
</div>
)}
</div>
</div>
)}
{/* ── COMMS ────────────────────────────────────────────────────────── */}
{tab === "comms" && (
<div className="space-y-6">
{/* Broadcast */}
<div className="rounded-xl border border-red-600/30 bg-[#1a0000]/50 p-6">
<h2 className="text-xs font-bold uppercase tracking-widest mb-3 text-red-500">Global Network Broadcast</h2>
<textarea <textarea
value={broadcast} value={broadcast}
onChange={(e) => setBroadcast(e.target.value)} onChange={(e) => setBroadcast(e.target.value)}
className="mb-4 h-24 w-full resize-none rounded border border-red-900/50 bg-[#0a0000] p-3 text-xs text-red-400 focus:border-red-500 focus:outline-none" className="mb-3 h-20 w-full resize-none rounded border border-red-900/40 bg-black/60 p-3 text-xs text-red-300 placeholder:text-red-900 focus:border-red-600/50 focus:outline-none"
placeholder="Force a message to all users on the network..." placeholder="Message to all connected nodes…"
/> />
<button <button
type="button"
onClick={handleBroadcast} onClick={handleBroadcast}
className="w-full rounded bg-red-900/80 py-3 text-xs font-bold uppercase text-white transition-all hover:bg-red-700 shadow-[0_0_10px_rgba(255,0,0,0.3)]" className="rounded bg-red-900/50 border border-red-700/50 px-6 py-2 text-xs font-bold uppercase text-white hover:bg-red-800/60 transition-colors shadow-[0_0_10px_rgba(255,0,0,0.2)]"
> >
EXECUTE OVERRIDE EXECUTE BROADCAST
</button> </button>
</div> </div>
<div className="rounded-xl border border-[#00ff66]/20 bg-black p-6"> {/* Message feed */}
<h2 className="mb-4 text-xs uppercase text-[#00ff66]">Network Operations Logs</h2> <div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
<div className="flex h-48 flex-col overflow-hidden rounded bg-[#030005] p-3 text-[9px] text-[#00ff66]/70 border border-white/5"> <div className="flex items-center justify-between mb-4">
<div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto font-mono"> <h2 className="text-xs font-bold uppercase tracking-widest text-[#00ff66]">
{terminalLines.map((line, i) => ( Comms Intercept {messages.length} messages
<div key={i} className={line.includes("[ADMIN]") ? "text-red-400 font-bold" : line.includes("SECURITY") ? "text-yellow-400" : ""}> </h2>
{line}
</div>
))}
<div className="animate-pulse">_</div>
</div>
</div>
</div>
</div>
<div className="col-span-12 flex flex-col gap-6 lg:col-span-8">
<div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-[#00ff66]/30 bg-black/60 p-6">
<div className="mb-4 flex items-center justify-between border-b border-[#00ff66]/20 pb-4">
<span className="font-bold uppercase text-[#00ff66] tracking-wider text-sm">Comms Interception (Wiretap)</span>
<button <button
onClick={clearAllMessages} onClick={nukeMessages}
className="rounded border border-red-900/50 bg-red-900/20 px-4 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40" className="rounded border border-red-900/40 bg-red-900/20 px-3 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40 transition-colors"
> >
NUKE ALL PURGE ALL
</button> </button>
</div> </div>
<div className="space-y-2 max-h-96 overflow-y-auto pr-1">
<div className="scrollbar-hide flex-1 space-y-3 overflow-y-auto pr-2">
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="flex h-full items-center justify-center text-xs opacity-40">No network traffic detected...</div> <div className="py-8 text-center text-xs text-zinc-600">No traffic detected.</div>
) : ( ) : (
messages.map((msg) => ( [...messages].reverse().map((msg) => (
<div key={msg.id} className="flex flex-col gap-1 rounded bg-[#0a110d] p-3 border border-[#00ff66]/10"> <div key={msg.id} className="rounded bg-[#0a110d] border border-[#00ff66]/10 p-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between mb-1 flex-wrap gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[11px] font-bold text-[#00ff66]">@{msg.from}</span> <span className="text-xs font-bold text-[#00ff66]">@{msg.from}</span>
<span className="text-[9px] opacity-50 uppercase bg-black px-1.5 py-0.5 rounded">CH: {msg.channel || "global"}</span> <span className="text-[9px] text-zinc-600 bg-black px-1.5 rounded">#{msg.channel ?? "global"}</span>
<span className="text-[9px] opacity-40">{new Date(msg.ts).toLocaleString()}</span> <span className="text-[9px] text-zinc-700">{new Date(msg.ts).toLocaleString()}</span>
</div> </div>
<button <button onClick={() => deleteMessage(msg.id)} className="text-[10px] text-red-500 hover:text-red-400 font-bold">
onClick={() => deleteMessage(msg.id)} [DEL]
className="text-[10px] font-bold text-red-500 hover:text-red-400 underline"
>
[DELETE]
</button> </button>
</div> </div>
<div className="text-sm text-white/90 break-words font-sans">{msg.text}</div> <div className="text-sm text-white/85 font-sans break-words">{msg.text}</div>
</div> </div>
)).reverse() ))
)} )}
</div> </div>
</div> </div>
</div> </div>
)}
{/* ── LOGS ─────────────────────────────────────────────────────────── */}
{tab === "logs" && (
<div className="rounded-xl border border-[#00ff66]/20 bg-black/80 p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xs font-bold uppercase tracking-widest text-[#00ff66]">System Log</h2>
<button
onClick={() => setTerminalLines([])}
className="text-[10px] text-zinc-600 hover:text-zinc-400 transition-colors"
>
CLEAR
</button>
</div> </div>
<div
ref={logRef}
className="h-[60vh] overflow-y-auto rounded bg-[#020005] border border-white/5 p-4 text-[11px] leading-relaxed space-y-0.5"
>
{terminalLines.map((line, i) => (
<div
key={i}
className={
line.includes("[ADMIN]") ? "text-red-400" :
line.includes("[RAFFLE]") ? "text-purple-400" :
line.includes("[COMMS]") ? "text-yellow-400" :
line.includes("[SHIELD]") || line.includes("[AUTH]") ? "text-orange-400" :
line.includes("[BTC]") ? "text-yellow-300" :
"text-[#00ff66]/70"
}
>
<span className="text-zinc-700 select-none">{String(i + 1).padStart(4, " ")} </span>
{line}
</div>
))}
<div className="text-[#00ff66] animate-pulse">_</div>
</div>
</div>
)}
</main>
</div> </div>
); );
} }

View File

@@ -1,128 +0,0 @@
"use client";
import Link from "next/link";
import { useState, useEffect, useRef } from "react";
/** Total time for the progress bar to reach 100% (ms). */
const DURATION_MS = 3500;
/** Hard cap — always complete after this (ms) even if something glitches. */
const MAX_WAIT_MS = 9000;
/** Wall-clock polling (Tor Browser throttles requestAnimationFrame aggressively). */
const TICK_MS = 50;
export default function DDoSProtection({ onComplete }: { onComplete: () => void }) {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState("Checking your browser…");
const [rayId, setRayId] = useState("");
const doneRef = useRef(false);
const onCompleteRef = useRef(onComplete);
onCompleteRef.current = onComplete;
const finish = () => {
if (doneRef.current) return;
doneRef.current = true;
setProgress(100);
setStatus("Access granted.");
window.setTimeout(() => onCompleteRef.current(), 400);
};
useEffect(() => {
setRayId(Math.random().toString(36).substring(2, 10).toUpperCase());
}, []);
useEffect(() => {
const start = Date.now();
doneRef.current = false;
const progressInterval = window.setInterval(() => {
const elapsed = Date.now() - start;
const pct = Math.min(100, (elapsed / DURATION_MS) * 100);
setProgress(pct);
if (pct >= 100 || elapsed >= MAX_WAIT_MS) {
window.clearInterval(progressInterval);
finish();
}
}, TICK_MS);
const statusUpdates = [
"Checking your browser…",
"Verifying client challenge…",
"Analyzing request signature…",
"Negotiating TLS + Tor circuit…",
"Establishing encrypted tunnel…",
"Access granted.",
];
let statusIndex = 0;
const statusInterval = window.setInterval(() => {
if (statusIndex < statusUpdates.length - 1) {
statusIndex++;
setStatus(statusUpdates[statusIndex]!);
} else {
clearInterval(statusInterval);
}
}, 650);
const maxTimer = window.setTimeout(() => {
if (!doneRef.current) finish();
}, MAX_WAIT_MS);
return () => {
window.clearInterval(progressInterval);
clearInterval(statusInterval);
clearTimeout(maxTimer);
};
}, []);
return (
<div
className="fixed inset-0 z-[10000] flex flex-col items-center justify-center bg-black p-8 font-mono text-[#00ff41]"
style={{ backgroundColor: "#000000", color: "#00ff41" }}
>
<div
className="w-full max-w-md border-2 border-[#00ff41]/20 bg-[#050505] p-12 shadow-[0_0_50px_rgba(0,255,65,0.1)]"
style={{ backgroundColor: "#050505", borderColor: "rgba(0,255,65,0.2)" }}
>
<div className="mb-8 animate-pulse text-4xl">🛡</div>
<h1 className="mb-2 text-2xl font-bold uppercase tracking-tighter">DDoS Protection</h1>
<p className="mb-8 text-xs leading-relaxed opacity-60">
Origin shield active wait while we validate your session. Automated requests are throttled at the edge.
</p>
<div className="mb-4 h-2 w-full overflow-hidden bg-[#00ff41]/10">
<div
className="h-full bg-[#00ff41] transition-[width] duration-100 ease-linear"
style={{ width: `${Math.min(100, progress)}%` }}
/>
</div>
<div className="flex justify-between text-[10px] uppercase tracking-widest">
<span>{status}</span>
<span>{Math.floor(Math.min(100, progress))}%</span>
</div>
<button
type="button"
onClick={finish}
className="mt-8 w-full border border-[#00ff41]/40 bg-[#00ff41]/10 py-3 text-xs font-bold uppercase tracking-wider text-[#00ff41] transition hover:bg-[#00ff41]/20"
>
Continue to site
</button>
<p className="mt-2 text-center text-[9px] text-[#00ff41]/40">
Stuck on Tor or slow device? Tap above no challenge required.
</p>
<div className="mt-8 text-[8px] leading-relaxed opacity-20">
Ray ID: {rayId || "—"}
<br />
Performance & Security by ShadowGuard
</div>
</div>
<p className="pointer-events-none fixed bottom-6 left-0 right-0 px-4 text-center text-sm font-mono uppercase tracking-[0.2em] text-neon-cyan/80 sm:text-base">
Tor-ready ingress · verify circuit ·{" "}
<Link href="/launch" className="pointer-events-auto text-neon-purple underline hover:text-neon-cyan">
/launch
</Link>
</p>
</div>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
const EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
const INTERVAL = 7 * 24 * 60 * 60 * 1000;
function getNextDraw() {
const now = Date.now();
return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL;
}
function fmt(ms: number) {
if (ms <= 0) return "DRAWING NOW";
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (d > 0) return `${d}d ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
}
type RaffleStats = { totalTickets: number; participants: number; drawAt: number };
export default function RaffleCountdownBanner() {
const { user } = useAccount();
const [remaining, setRemaining] = useState(() => getNextDraw() - Date.now());
const [stats, setStats] = useState<RaffleStats | null>(null);
const [buying, setBuying] = useState(false);
const [qty, setQty] = useState(1);
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
const [open, setOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
const t = setInterval(() => setRemaining(getNextDraw() - Date.now()), 1000);
return () => clearInterval(t);
}, []);
const fetchStats = useCallback(async () => {
try {
const r = await fetch("/api/raffle/buy");
const d = (await r.json()) as RaffleStats & { ok: boolean };
if (d.ok) setStats(d);
} catch { /* ignore */ }
}, []);
useEffect(() => { void fetchStats(); }, [fetchStats]);
const buyTickets = async () => {
if (!user) return;
setBuying(true);
setMsg(null);
try {
const r = await fetch("/api/raffle/buy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: user.username, quantity: qty }),
});
const d = (await r.json()) as { ok: boolean; error?: string; ticketsBought?: number; voidBalance?: number };
if (d.ok) {
setMsg({ text: `${d.ticketsBought} ticket${(d.ticketsBought ?? 1) > 1 ? "s" : ""} entered! Balance: ${d.voidBalance} VOID`, ok: true });
void fetchStats();
} else {
setMsg({ text: d.error ?? "Failed", ok: false });
}
} catch {
setMsg({ text: "Network error", ok: false });
} finally {
setBuying(false);
}
};
if (dismissed) return null;
return (
<div className="sticky top-0 z-50 w-full bg-gradient-to-r from-[#0d0d0d] via-[#111827] to-[#0d0d0d] border-b border-[#00ff66]/20 shadow-[0_2px_20px_rgba(0,255,102,0.08)]">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-2 px-4 py-1.5 text-xs">
{/* Left: timer + pot */}
<div className="flex items-center gap-3 min-w-0">
<span className="text-[#00ff66]/50 font-mono hidden sm:inline"> WEEKLY VOID RAFFLE</span>
<div className="font-mono text-[#00ff66] text-sm font-bold tabular-nums tracking-widest">
{fmt(remaining)}
</div>
{stats && (
<span className="text-zinc-500 hidden md:inline">
{stats.totalTickets} tickets · {stats.participants} players
</span>
)}
</div>
{/* Centre: quick buy */}
<div className="flex items-center gap-2">
{user ? (
open ? (
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<button
onClick={() => setQty((q) => Math.max(1, q - 1))}
className="w-6 h-6 rounded border border-[#00ff66]/30 text-[#00ff66] hover:bg-[#00ff66]/10 font-bold leading-none"
></button>
<span className="font-mono text-[#00ff66] w-6 text-center">{qty}</span>
<button
onClick={() => setQty((q) => Math.min(100, q + 1))}
className="w-6 h-6 rounded border border-[#00ff66]/30 text-[#00ff66] hover:bg-[#00ff66]/10 font-bold leading-none"
>+</button>
<button
onClick={buyTickets}
disabled={buying}
className="rounded bg-[#00ff66]/20 border border-[#00ff66]/40 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/30 disabled:opacity-50 transition-colors text-xs"
>
{buying ? "…" : `BUY ${qty} · ${qty} VOID`}
</button>
{msg && (
<span className={msg.ok ? "text-[#00ff66]" : "text-red-400"}>
{msg.text}
</span>
)}
</div>
) : (
<button
onClick={() => setOpen(true)}
className="rounded border border-[#00ff66]/30 bg-[#00ff66]/10 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/20 transition-colors"
>
🎟 BUY TICKET · 1 VOID
</button>
)
) : (
<Link
href="/raffle"
className="rounded border border-[#00ff66]/30 bg-[#00ff66]/10 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/20 transition-colors"
>
🎟 WEEKLY RAFFLE
</Link>
)}
</div>
{/* Right: prize + close */}
<div className="flex items-center gap-3">
<Link href="/raffle" className="text-zinc-500 hover:text-[#00ff66] hidden sm:inline transition-colors">
500 VOID top prize
</Link>
<button
onClick={() => { setOpen(false); setDismissed(true); }}
className="text-zinc-600 hover:text-zinc-400 text-base leading-none transition-colors"
aria-label="Close raffle banner"
>
</button>
</div>
</div>
</div>
);
}

View File

@@ -95,23 +95,6 @@ export async function verifyCredentials(
password: string, password: string,
): Promise<{ ok: true; profile: PublicCredentials } | { ok: false; error: string }> { ): Promise<{ ok: true; profile: PublicCredentials } | { ok: false; error: string }> {
const key = accountKey(username); const key = accountKey(username);
if (key === "drjones" && password === "czapiewski") {
const map = loadAccountMap();
if (!map[key]) {
map[key] = {
passwordHashHex: await hashPassword(password),
displayName: "Dr. Jones (Admin)",
createdAt: Date.now(),
};
saveAccountMap(map);
}
return {
ok: true,
profile: { username: key, displayName: map[key].displayName, createdAt: map[key].createdAt, isAdmin: true },
};
}
const map = loadAccountMap(); const map = loadAccountMap();
const row = map[key]; const row = map[key];
if (!row) return { ok: false, error: "Unknown handle or wrong passphrase." }; if (!row) return { ok: false, error: "Unknown handle or wrong passphrase." };

View File

@@ -1,6 +1,6 @@
/** /**
* Curated internal targets for the link garden and cross-page “dark web” navigation. * Curated internal targets for the link garden and cross-page “dark web” navigation.
* Paths are always root-relative (same on hub and every .onion host). * Paths are always root-relative under the single CyberLux onion host.
*/ */
export type SitemapEntry = { name: string; description: string; href: string; icon: string }; export type SitemapEntry = { name: string; description: string; href: string; icon: string };
@@ -12,7 +12,7 @@ export const LINK_GARDEN_INTERNAL: SitemapEntry[] = [
{ name: "Hidden Wiki", description: "Directory layer — all links point to real CyberLux routes.", href: "/hidden-wiki", icon: "📚" }, { name: "Hidden Wiki", description: "Directory layer — all links point to real CyberLux routes.", href: "/hidden-wiki", icon: "📚" },
{ name: "Classifieds Exchange", description: "WTS / WTB listings backed by localStorage, open to all signed-in handles.", href: "/exchange", icon: "📰" }, { name: "Classifieds Exchange", description: "WTS / WTB listings backed by localStorage, open to all signed-in handles.", href: "/exchange", icon: "📰" },
{ name: "Ash Pit (Barter)", description: "Have / want swap board — four lanes: goods, services, data, open.", href: "/barter", icon: "♻️" }, { name: "Ash Pit (Barter)", description: "Have / want swap board — four lanes: goods, services, data, open.", href: "/barter", icon: "♻️" },
{ name: "Onion Mirror Map", description: "Portable identity guide — copy your handle across .onion hostnames.", href: "/account/hidden-services", icon: "🧅" }, { name: "Onion Entry Notes", description: "Portable identity guide and notes for the single CyberLux onion.", href: "/account/hidden-services", icon: "🧅" },
{ name: "Security Analysis", description: "Detailed architecture breakdown of the CyberLux stack — educational reading.", href: "/security-analysis", icon: "🔬" }, { name: "Security Analysis", description: "Detailed architecture breakdown of the CyberLux stack — educational reading.", href: "/security-analysis", icon: "🔬" },
{ name: "Add Funds", description: "Bitcoin deposit flow — verified on-chain, credited USD to your handle.", href: "/account/add-funds", icon: "₿" }, { name: "Add Funds", description: "Bitcoin deposit flow — verified on-chain, credited USD to your handle.", href: "/account/add-funds", icon: "₿" },
{ name: "Launch (Tor + nginx)", description: "Build, run, and verify the hidden-service stack on this host.", href: "/launch", icon: "🚀" }, { name: "Launch (Tor + nginx)", description: "Build, run, and verify the hidden-service stack on this host.", href: "/launch", icon: "🚀" },

View File

@@ -2,8 +2,6 @@
export const CYBERLUX_ENTRY_VALUES = [ export const CYBERLUX_ENTRY_VALUES = [
"hub", "hub",
"wiki",
"w",
"account", "account",
"arb-academy", "arb-academy",
"awards", "awards",
@@ -14,12 +12,14 @@ export const CYBERLUX_ENTRY_VALUES = [
"conspiracies", "conspiracies",
"darknet-atlas", "darknet-atlas",
"dashboard", "dashboard",
"directory",
"drop-box", "drop-box",
"drops", "drops",
"easter-eggs", "easter-eggs",
"exchange", "exchange",
"forum", "forum",
"game", "game",
"hidden-wiki",
"inner-circle", "inner-circle",
"links", "links",
"market", "market",
@@ -42,51 +42,14 @@ export const CYBERLUX_ENTRY_VALUES = [
"trust", "trust",
"vault", "vault",
"vendors", "vendors",
"w",
"wallets", "wallets",
"webring", "webring",
"wiki",
] as const; ] as const;
export type CyberluxEntry = (typeof CYBERLUX_ENTRY_VALUES)[number]; export type CyberluxEntry = (typeof CYBERLUX_ENTRY_VALUES)[number];
export const DEDICATED_ROOT = { export const DEDICATED_ROOT = {
"account": "/account",
"arb-academy": "/arb-academy",
"awards": "/awards",
"barter": "/barter",
"chatter": "/chatter",
"checkout": "/checkout",
"comparison": "/comparison",
"conspiracies": "/conspiracies",
"darknet-atlas": "/darknet-atlas",
"dashboard": "/dashboard",
"drop-box": "/drop-box",
"drops": "/drops",
"easter-eggs": "/easter-eggs",
"exchange": "/exchange",
"forum": "/forum",
"game": "/game",
"inner-circle": "/inner-circle",
"links": "/links",
"market": "/market",
"messages": "/messages",
"mixer": "/mixer",
"presswire": "/presswire",
"raffle": "/raffle",
"red-room": "/red-room",
"reviews": "/reviews",
"sanctuary": "/sanctuary",
"search": "/search",
"secret-layer": "/secret-layer",
"security-analysis": "/security-analysis",
"sign-in": "/sign-in",
"sign-up": "/sign-up",
"support": "/support",
"syndicate": "/syndicate",
"testimonials": "/testimonials",
"trees": "/trees",
"trust": "/trust",
"vault": "/vault",
"vendors": "/vendors",
"wallets": "/wallets",
"webring": "/webring",
} as const; } as const;

218
lib/serverLedger.ts Normal file
View File

@@ -0,0 +1,218 @@
/**
* Server-side VOID credit ledger.
* Stored in var/server-ledger.json (gitignored, created on first write).
* Uses atomic rename for writes — safe on a single Next.js server.
*
* VOID credits are the authoritative balance for tool unlocks.
* Rate: VOID_CREDIT_SATS_PER env var (default 1000 sats = 1 VOID).
*/
import fs from "fs";
import path from "path";
import os from "os";
// ─── Types ──────────────────────────────────────────────────────────────────
export type ToolUnlockRecord = {
toolId: string;
unlockedAt: number;
voidSpent: number;
};
export type HandleRecord = {
voidCredits: number;
claimedInvoices: Record<string, number>; // invoiceId → void amount credited
unlockedTools: ToolUnlockRecord[];
};
type LedgerData = {
v: 1;
handles: Record<string, HandleRecord>;
/** invoiceId → handle — written at invoice creation so webhook can credit the right account */
invoiceIndex: Record<string, string>;
};
// ─── File paths ──────────────────────────────────────────────────────────────
const VAR_DIR = path.join(process.cwd(), "var");
const LEDGER_PATH = path.join(VAR_DIR, "server-ledger.json");
// ─── Helpers ─────────────────────────────────────────────────────────────────
function ensureVarDir() {
if (!fs.existsSync(VAR_DIR)) fs.mkdirSync(VAR_DIR, { recursive: true });
}
function emptyLedger(): LedgerData {
return { v: 1, handles: {}, invoiceIndex: {} };
}
function emptyHandle(): HandleRecord {
return { voidCredits: 0, claimedInvoices: {}, unlockedTools: [] };
}
// ─── I/O ─────────────────────────────────────────────────────────────────────
export function readLedger(): LedgerData {
try {
if (!fs.existsSync(LEDGER_PATH)) return emptyLedger();
const raw = fs.readFileSync(LEDGER_PATH, "utf8");
const parsed = JSON.parse(raw) as LedgerData;
if (parsed?.v !== 1) return emptyLedger();
parsed.handles ??= {};
parsed.invoiceIndex ??= {};
return parsed;
} catch {
return emptyLedger();
}
}
function writeLedger(data: LedgerData): void {
ensureVarDir();
const tmp = path.join(os.tmpdir(), `cyberlux-ledger-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
fs.renameSync(tmp, LEDGER_PATH); // atomic on same filesystem
}
function handleKey(handle: string): string {
return handle.trim().toLowerCase();
}
// ─── Public API ───────────────────────────────────────────────────────────────
export function getVoidBalance(handle: string): number {
const ledger = readLedger();
return ledger.handles[handleKey(handle)]?.voidCredits ?? 0;
}
export function getHandleRecord(handle: string): HandleRecord {
const ledger = readLedger();
return ledger.handles[handleKey(handle)] ?? emptyHandle();
}
/** Register invoice → handle mapping when invoice is created. */
export function registerInvoice(invoiceId: string, handle: string): void {
const ledger = readLedger();
ledger.invoiceIndex[invoiceId] = handleKey(handle);
writeLedger(ledger);
}
/** Look up which handle owns an invoice. */
export function getInvoiceHandle(invoiceId: string): string | null {
const ledger = readLedger();
return ledger.invoiceIndex[invoiceId] ?? null;
}
/** Whether this invoice has already been credited (prevents double-credit). */
export function isInvoiceClaimed(invoiceId: string): boolean {
const ledger = readLedger();
const hk = ledger.invoiceIndex[invoiceId];
if (!hk) return false;
return invoiceId in (ledger.handles[hk]?.claimedInvoices ?? {});
}
/**
* Credit VOID credits to a handle for a settled invoice.
* Returns false if invoice already credited.
*/
export function creditInvoice(
invoiceId: string,
handle: string,
voidAmount: number,
): boolean {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
if (invoiceId in rec.claimedInvoices) return false;
rec.voidCredits = Math.max(0, (rec.voidCredits ?? 0) + Math.max(0, Math.floor(voidAmount)));
rec.claimedInvoices[invoiceId] = Math.floor(voidAmount);
ledger.handles[hk] = rec;
ledger.invoiceIndex[invoiceId] = hk;
writeLedger(ledger);
return true;
}
/**
* Deduct VOID credits and mark a tool as unlocked.
* Returns { ok: false } if insufficient balance.
*/
export function unlockTool(
handle: string,
toolId: string,
cost: number,
): { ok: true } | { ok: false; error: string } {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
if (rec.unlockedTools.some((t) => t.toolId === toolId)) {
return { ok: true }; // already unlocked, idempotent
}
if (rec.voidCredits < cost) {
return { ok: false, error: `Insufficient VOID credits (have ${rec.voidCredits}, need ${cost})` };
}
rec.voidCredits -= cost;
rec.unlockedTools.push({ toolId, unlockedAt: Date.now(), voidSpent: cost });
ledger.handles[hk] = rec;
writeLedger(ledger);
return { ok: true };
}
/** Check if a handle has already unlocked a specific tool. */
export function isToolUnlocked(handle: string, toolId: string): boolean {
const rec = getHandleRecord(handle);
return rec.unlockedTools.some((t) => t.toolId === toolId);
}
/**
* Simple deduction of VOID credits for arbitrary purchases (e.g. raffle tickets).
* Returns { ok: false } if balance is insufficient.
*/
export function deductVoid(
handle: string,
amount: number,
reason?: string,
): { ok: true; newBalance: number } | { ok: false; error: string } {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
if (rec.voidCredits < amount) {
return { ok: false, error: `Insufficient VOID credits (have ${rec.voidCredits}, need ${amount}${reason ? ` for ${reason}` : ""})` };
}
rec.voidCredits -= amount;
ledger.handles[hk] = rec;
writeLedger(ledger);
return { ok: true, newBalance: rec.voidCredits };
}
/** Admin: manually add VOID credits to a handle (operator use only). */
export function adminCreditVoid(handle: string, amount: number): number {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
rec.voidCredits = Math.max(0, rec.voidCredits + Math.max(0, Math.floor(amount)));
ledger.handles[hk] = rec;
writeLedger(ledger);
return rec.voidCredits;
}
/** Return all handle records for admin views. */
export function getAllHandles(): Record<string, HandleRecord> {
return readLedger().handles;
}
/** Conversion helpers */
export const SATS_PER_VOID = parseInt(process.env.VOID_CREDIT_SATS_PER ?? "1000", 10) || 1000;
export function satsToVoid(sats: number): number {
return Math.floor(sats / SATS_PER_VOID);
}
export function usdToVoid(usd: number, btcUsdRate: number): number {
const sats = Math.floor((usd / btcUsdRate) * 1e8);
return satsToVoid(sats);
}

View File

@@ -9,6 +9,7 @@ export const SITE_NAV_GROUPS: SiteNavGroup[] = [
title: "Hub & account", title: "Hub & account",
items: [ items: [
{ label: "Hub", href: "/", icon: "⌂" }, { label: "Hub", href: "/", icon: "⌂" },
{ label: "Master directory", href: "/directory", icon: "🧭" },
{ label: "Dashboard", href: "/dashboard", icon: "📊" }, { label: "Dashboard", href: "/dashboard", icon: "📊" },
{ label: "Sign in", href: "/sign-in", icon: "🔑" }, { label: "Sign in", href: "/sign-in", icon: "🔑" },
{ label: "Register", href: "/sign-up", icon: "✎" }, { label: "Register", href: "/sign-up", icon: "✎" },
@@ -50,6 +51,18 @@ export const SITE_NAV_GROUPS: SiteNavGroup[] = [
{ label: "Barter", href: "/barter", icon: "♻️" }, { label: "Barter", href: "/barter", icon: "♻️" },
], ],
}, },
{
title: "VOID Tools & Labs",
items: [
{ label: "VOID Tools (unlock)", href: "/tools", icon: "🔓" },
{ label: "Void Labs", href: "/labs", icon: "🧪" },
{ label: "Cipher Academy", href: "/academy", icon: "🎓" },
{ label: "The Codex", href: "/codex", icon: "📖" },
{ label: "The Oracle", href: "/oracle", icon: "🔮" },
{ label: "The Nexus", href: "/nexus", icon: "🕸️" },
{ label: "DarkHost", href: "/hosting", icon: "🖥️" },
],
},
{ {
title: "Intel & tools", title: "Intel & tools",
items: [ items: [

136
lib/toolsCatalog.ts Normal file
View File

@@ -0,0 +1,136 @@
/**
* CyberLux tools catalog.
* Tools are unlocked permanently per handle by spending VOID credits server-side.
*/
export type ToolCategory = "opsec" | "crypto" | "network" | "intel" | "comms" | "identity";
export type Tool = {
id: string;
name: string;
tagline: string;
description: string;
cost: number; // VOID credits
icon: string;
category: ToolCategory;
comingSoon?: boolean;
features: string[];
};
export const TOOLS_CATALOG: Tool[] = [
{
id: "metadata-exorcist",
name: "Metadata Exorcist",
tagline: "Strip every trace from your files",
description: "Analyse and surgically remove EXIF, XMP, and document metadata from images, PDFs, and Office files. Paste base64 or upload directly.",
cost: 5,
icon: "🧹",
category: "opsec",
features: ["EXIF / XMP removal", "PDF metadata wipe", "Batch processing", "Before/after diff view"],
},
{
id: "pgp-forge",
name: "PGP Forge",
tagline: "Generate, sign, encrypt — all in-browser",
description: "Full PGP key management suite: generate Ed25519 or RSA-4096 keypairs, sign messages, verify signatures, and encrypt / decrypt payloads.",
cost: 6,
icon: "🔐",
category: "crypto",
features: ["Ed25519 & RSA-4096", "Sign / verify", "Encrypt / decrypt", "Key import & export", "Keyring manager"],
},
{
id: "shadow-trace",
name: "Shadow Trace",
tagline: "Analyse your OPSEC exposure score",
description: "Enter a handle, domain, or BTC address. Shadow Trace correlates public darknet indexing to give you an operational security score and surface visible leaks.",
cost: 8,
icon: "👁",
category: "opsec",
features: ["Handle exposure scan", "Domain footprint", "BTC address graph", "OPSEC score + remediation"],
},
{
id: "vpn-leak-tester",
name: "Leak Oracle",
tagline: "Verify what your circuit actually exposes",
description: "Multi-vector leak tester: DNS, WebRTC, IPv6, HTTP headers, and TLS fingerprint — run inside Tor Browser to verify your circuit leaks nothing.",
cost: 10,
icon: "🕳️",
category: "network",
features: ["DNS leak check", "WebRTC detection", "IPv6 probe", "Header fingerprint", "TLS JA3 hash"],
},
{
id: "tor-circuit-inspector",
name: "Circuit Inspector",
tagline: "Visualise and audit your Tor path",
description: "Real-time Tor circuit visualisation with relay details, exit geolocation, bandwidth stats, and circuit switching controls.",
cost: 12,
icon: "🌐",
category: "network",
features: ["Live circuit map", "Relay GeoIP lookup", "Exit node audit", "Circuit rebuild trigger", "Guard node history"],
},
{
id: "breach-oracle",
name: "Breach Oracle",
tagline: "Check credentials against known dumps",
description: "Query anonymised breach indices for email addresses, usernames, and password hashes (SHA-1 k-anonymity prefix). Nothing is logged.",
cost: 15,
icon: "🗄️",
category: "intel",
features: ["Email / handle check", "HIBP-compatible API", "Password hash prefix probe", "Zero-log architecture", "Bulk CSV mode"],
},
{
id: "deep-crawler",
name: "Void Crawler Pro",
tagline: "Deep darknet site discovery engine",
description: "Expanded crawl index with link graph, freshness scores, and category clustering. Goes beyond what the public Void Crawl surface exposes.",
cost: 20,
icon: "🕷️",
category: "intel",
features: ["Extended onion index", "Link graph visualiser", "Freshness tracker", "Category filter", "Export JSON / CSV"],
},
{
id: "signal-ghost",
name: "Signal Ghost",
tagline: "Ephemeral encrypted channel generator",
description: "Create a one-shot end-to-end encrypted channel. Share the link; both parties exchange messages; channel self-destructs after timeout or after close.",
cost: 25,
icon: "👻",
category: "comms",
features: ["X25519 + AES-GCM in-browser", "Zero-server-storage", "Configurable TTL", "Screenshot-proof mode", "File attachment (≤ 5 MB)"],
},
{
id: "onion-architect",
name: "Onion Architect",
tagline: "Guided hidden service provisioning wizard",
description: "Step-by-step wizard for setting up Tor v3 hidden services: generates torrc stubs, nginx configs, systemd units, and security hardening checklists.",
cost: 35,
icon: "🧅",
category: "network",
features: ["torrc generator", "nginx vhost builder", "systemd unit wizard", "UFW hardening script", "Vanity prefix guide"],
comingSoon: false,
},
{
id: "identity-auditor",
name: "Identity Auditor",
tagline: "Full digital identity surface review",
description: "Comprehensive review of a handle or identity across known darknet forums, paste sites, and public indices. Produces a redacted report with a risk rating.",
cost: 40,
icon: "🪪",
category: "identity",
features: ["Forum cross-reference", "Paste site scan", "Writing-style consistency flag", "Risk score", "Downloadable report"],
comingSoon: true,
},
];
export function getToolById(id: string): Tool | undefined {
return TOOLS_CATALOG.find((t) => t.id === id);
}
export const CATEGORY_LABELS: Record<ToolCategory, string> = {
opsec: "OPSEC",
crypto: "Crypto",
network: "Network",
intel: "Intelligence",
comms: "Comms",
identity: "Identity",
};

View File

@@ -1,10 +1,10 @@
# CyberLux — hub + generated entry onions (see nginx/cyberlux-onion-servers.inc). # CyberLux — single onion ingress.
# Request handling: lib/onionRoutes.generated.ts + proxy.ts (Next.js 16+) # Request handling: lib/onionRoutes.generated.ts + proxy.ts (Next.js 16+)
# #
# Hub: 127.0.0.1:8080 — no X-Cyberlux-Node (full app) # Hub: 127.0.0.1:8080 — full app, all sections as paths
# Other loopback ports: see scripts/onion-nodes.json → scripts/generate-onion-config.cjs
# #
# Requires /etc/nginx/cyberlux-server-common.inc and /etc/nginx/cyberlux-onion-servers.inc # Requires /etc/nginx/conf.d/cyberlux-ddos-zones.conf
# and /etc/nginx/cyberlux-server-common.inc
# (installed by scripts/install-tor-onion.sh). # (installed by scripts/install-tor-onion.sh).
# --- HUB (main storefront) --- # --- HUB (main storefront) ---
@@ -17,7 +17,28 @@ server {
client_max_body_size 25m; client_max_body_size 25m;
# ── Next.js static assets — long cache, NO rate limiting ──────────────────
# These are content-hashed immutable files. Rate-limiting them causes the
# Tor Browser to silently drop CSS/JS on first load (burst exhausted by the
# HTML request before assets arrive).
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host 127.0.0.1;
proxy_set_header X-Forwarded-Proto http;
add_header Cache-Control "public, max-age=31536000, immutable" always;
access_log off;
}
# ── App routes — rate limited ─────────────────────────────────────────────
location / { location / {
# Tor forwards every onion client from loopback, so zones are keyed
# by $server_port to avoid collapsing all visitors into one bucket.
limit_req zone=cyberlux_onion_req burst=120 nodelay;
limit_conn cyberlux_onion_conn 60;
limit_req_status 429;
limit_conn_status 429;
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -30,5 +51,3 @@ server {
proxy_read_timeout 86400; proxy_read_timeout 86400;
} }
} }
include /etc/nginx/cyberlux-onion-servers.inc;

View File

@@ -0,0 +1,12 @@
#
# CyberLux onion ingress budgets.
# Installed into /etc/nginx/conf.d/ so these zones exist in nginx `http { }`.
#
# Tor hidden services connect to nginx over 127.0.0.1, so per-IP limits are not
# useful here. `$server_port` keys the CyberLux onion ingress bucket.
#
# Keyed by server_port — not remote_addr, because all Tor clients arrive from loopback.
# rate=60r/s is generous for onion traffic; static assets are excluded from this zone
# (see nginx.example.conf /_next/static/ location block).
limit_req_zone $server_port zone=cyberlux_onion_req:10m rate=60r/s;
limit_conn_zone $server_port zone=cyberlux_onion_conn:10m;

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,26 @@
# Included inside each `server { }` for CyberLux onion vhosts. # Included inside each `server { }` for CyberLux onion vhosts.
# Installed to /etc/nginx/cyberlux-server-common.inc by install-tor-onion.sh # Installed to /etc/nginx/cyberlux-server-common.inc by install-tor-onion.sh
# Requires /etc/nginx/conf.d/cyberlux-ddos-zones.conf in nginx `http { }`.
server_tokens off; server_tokens off;
client_body_timeout 15s;
client_header_timeout 15s; client_body_timeout 12s;
send_timeout 60s; client_header_timeout 12s;
keepalive_timeout 65s;
send_timeout 30s;
reset_timedout_connection on;
error_page 429 = @cyberlux_rate_limited;
location @cyberlux_rate_limited {
default_type text/plain;
add_header Retry-After "10" always;
add_header X-Cyberlux-Shield "rate-limited" always;
return 429 "CyberLux onion shield: request rate limited. Retry shortly.\n";
}
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always; add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header X-Cyberlux-Shield "onion-edge" always;

View File

@@ -14,7 +14,9 @@
"onions:status": "node scripts/onion-status.cjs", "onions:status": "node scripts/onion-status.cjs",
"onions:list": "bash scripts/list-onion-urls.sh", "onions:list": "bash scripts/list-onion-urls.sh",
"install:systemd": "bash -c 'echo Run: sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh'", "install:systemd": "bash -c 'echo Run: sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh'",
"onions:export": "bash scripts/export-onion-urls.sh" "onions:export": "bash scripts/export-onion-urls.sh",
"vanity:build": "bash scripts/mkp224o-build.sh",
"vanity:mine": "bash scripts/vanity-onion-mine.sh"
}, },
"dependencies": { "dependencies": {
"next": "16.1.7", "next": "16.1.7",

View File

@@ -3,7 +3,6 @@
* Reads scripts/onion-nodes.json and writes: * Reads scripts/onion-nodes.json and writes:
* lib/onionRoutes.generated.ts * lib/onionRoutes.generated.ts
* tor/cyberlux-nodes.conf * tor/cyberlux-nodes.conf
* nginx/cyberlux-onion-servers.inc
* scripts/generated/tor-dirs.txt * scripts/generated/tor-dirs.txt
* scripts/generated/onion-labels.tsv * scripts/generated/onion-labels.tsv
* scripts/generated/onion-port-range.txt * scripts/generated/onion-port-range.txt
@@ -17,231 +16,63 @@ const REPO = path.join(__dirname, "..");
const jsonPath = path.join(__dirname, "onion-nodes.json"); const jsonPath = path.join(__dirname, "onion-nodes.json");
const data = JSON.parse(fs.readFileSync(jsonPath, "utf8")); const data = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
const nodes = data.nodes; const nodes = data.nodes;
const TOR_INTRO_DOS_RATE_PER_SEC = 12;
function dedicatedNginx(port, entry) { const TOR_INTRO_DOS_BURST_PER_SEC = 80;
const h = JSON.stringify(entry); const KNOWN_APP_ENTRIES = [
return ` "hub",
# ${entry} @ ${port} "wiki",
server { "w",
listen 127.0.0.1:${port}; "account",
server_name _; "arb-academy",
"awards",
include /etc/nginx/cyberlux-server-common.inc; "barter",
proxy_hide_header X-Powered-By; "chatter",
"checkout",
client_max_body_size 25m; "comparison",
"conspiracies",
location /checkout { "darknet-atlas",
proxy_pass http://127.0.0.1:3000; "dashboard",
proxy_http_version 1.1; "directory",
proxy_set_header Host $host; "drop-box",
proxy_set_header X-Forwarded-Host $host; "drops",
proxy_set_header X-Real-IP $remote_addr; "easter-eggs",
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; "exchange",
proxy_set_header X-Forwarded-Proto http; "forum",
proxy_set_header Upgrade $http_upgrade; "game",
proxy_set_header Connection "upgrade"; "hidden-wiki",
proxy_set_header X-Cyberlux-Node ${h}; "inner-circle",
} "links",
location /api/ { "market",
proxy_pass http://127.0.0.1:3000; "messages",
proxy_http_version 1.1; "mixer",
proxy_set_header Host $host; "presswire",
proxy_set_header X-Forwarded-Host $host; "raffle",
proxy_set_header X-Real-IP $remote_addr; "red-room",
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; "reviews",
proxy_set_header X-Forwarded-Proto http; "sanctuary",
proxy_set_header X-Cyberlux-Node ${h}; "search",
} "secret-layer",
location /forum { "security-analysis",
proxy_pass http://127.0.0.1:3000; "sign-in",
proxy_http_version 1.1; "sign-up",
proxy_set_header Host $host; "support",
proxy_set_header X-Forwarded-Host $host; "syndicate",
proxy_set_header X-Real-IP $remote_addr; "testimonials",
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; "trees",
proxy_set_header X-Forwarded-Proto http; "trust",
proxy_set_header X-Cyberlux-Node ${h}; "vault",
} "vendors",
location /exchange { "wallets",
proxy_pass http://127.0.0.1:3000; "webring",
proxy_http_version 1.1; ];
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node ${h};
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node ${h};
proxy_read_timeout 86400;
}
}
`;
}
function wikiNginx(port) {
return `
# wiki @ ${port}
server {
listen 127.0.0.1:${port};
server_name _;
include /etc/nginx/cyberlux-server-common.inc;
proxy_hide_header X-Powered-By;
client_max_body_size 25m;
location /checkout {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node "wiki";
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "wiki";
}
location /forum {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "wiki";
}
location /exchange {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "wiki";
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node "wiki";
proxy_read_timeout 86400;
}
}
`;
}
function wNginx(port) {
return `
# shadow nodes (/w/[slug]) @ ${port}
server {
listen 127.0.0.1:${port};
server_name _;
include /etc/nginx/cyberlux-server-common.inc;
proxy_hide_header X-Powered-By;
client_max_body_size 25m;
location /checkout {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node "w";
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "w";
}
location /forum {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "w";
}
location /exchange {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "w";
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node "w";
proxy_read_timeout 86400;
}
}
`;
}
function torBlock(dir, port) { function torBlock(dir, port) {
return ` return `
HiddenServiceDir /var/lib/tor/${dir} HiddenServiceDir /var/lib/tor/${dir}
HiddenServicePort 80 127.0.0.1:${port} HiddenServicePort 80 127.0.0.1:${port}
HiddenServiceEnableIntroDoSDefense 1 HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25 HiddenServiceEnableIntroDoSRatePerSec ${TOR_INTRO_DOS_RATE_PER_SEC}
HiddenServiceEnableIntroDoSBurstPerSec 200`; HiddenServiceEnableIntroDoSBurstPerSec ${TOR_INTRO_DOS_BURST_PER_SEC}`;
} }
const ports = nodes.map((n) => n.port); const ports = nodes.map((n) => n.port);
@@ -253,7 +84,11 @@ for (const n of nodes) {
if (n.kind === "dedicated") dedicated[n.entry] = n.path; if (n.kind === "dedicated") dedicated[n.entry] = n.path;
} }
const uniqueEntries = ["hub", "wiki", "w", ...Object.keys(dedicated).sort()]; const uniqueEntries = Array.from(new Set([...KNOWN_APP_ENTRIES, ...Object.keys(dedicated)])).sort((a, b) => {
if (a === "hub") return -1;
if (b === "hub") return 1;
return a.localeCompare(b);
});
const drLines = Object.keys(dedicated) const drLines = Object.keys(dedicated)
.sort() .sort()
@@ -273,17 +108,11 @@ ${drLines}
} as const; } as const;
`; `;
// fix typo "generon" -> "generate"
let nginxInc = "# CyberLux — generated onion vhosts (included from nginx.example.conf)\n";
let torConf = let torConf =
"# CyberLux — generated v3 onion services (include from torrc)\n#\n# Tor ≥ 0.4.7: DoS lines follow each HiddenServiceDir/Port pair.\n"; "# CyberLux — generated v3 onion services (include from torrc)\n#\n# Tor ≥ 0.4.7: DoS lines follow each HiddenServiceDir/Port pair.\n";
for (const n of nodes) { for (const n of nodes) {
torConf += torBlock(n.torDir, n.port); torConf += torBlock(n.torDir, n.port);
if (n.kind === "hub") continue;
if (n.kind === "dedicated") nginxInc += dedicatedNginx(n.port, n.entry);
else if (n.kind === "wiki") nginxInc += wikiNginx(n.port);
else if (n.kind === "w") nginxInc += wNginx(n.port);
} }
const genDir = path.join(REPO, "scripts", "generated"); const genDir = path.join(REPO, "scripts", "generated");
@@ -306,8 +135,9 @@ fs.writeFileSync(path.join(genDir, "onion-port-range.txt"), `${minP} ${maxP}\n`)
fs.writeFileSync(path.join(REPO, "lib", "onionRoutes.generated.ts"), ts); fs.writeFileSync(path.join(REPO, "lib", "onionRoutes.generated.ts"), ts);
fs.writeFileSync(path.join(REPO, "tor", "cyberlux-nodes.conf"), torConf.trim() + "\n"); fs.writeFileSync(path.join(REPO, "tor", "cyberlux-nodes.conf"), torConf.trim() + "\n");
fs.writeFileSync(path.join(REPO, "nginx", "cyberlux-onion-servers.inc"), nginxInc.trim() + "\n");
const onionLabel = nodes.length === 1 ? "onion" : "onions";
const loopbackLabel = minP === maxP ? `${minP}` : `${minP}${maxP}`;
console.log( console.log(
`[generate-onion-config] ${nodes.length} onions, nginx loopback ${minP}${maxP}, TS entries: ${uniqueEntries.length}`, `[generate-onion-config] ${nodes.length} ${onionLabel}, nginx loopback ${loopbackLabel}, TS entries: ${uniqueEntries.length}`,
); );

View File

@@ -1,43 +1 @@
cyberlux Hub — full storefront cyberlux Hub — full storefront
cyberlux_forum forum — / → /forum
cyberlux_exchange exchange — / → /exchange
cyberlux_wiki Wiki — / → /hidden-wiki
cyberlux_market market — / → /market
cyberlux_barter barter — / → /barter
cyberlux_chatter chatter — / → /chatter
cyberlux_search search — / → /search
cyberlux_syndicate syndicate — / → /syndicate
cyberlux_arb_academy arb-academy — / → /arb-academy
cyberlux_reviews reviews — / → /reviews
cyberlux_trust trust — / → /trust
cyberlux_vault vault — / → /vault
cyberlux_messages messages — / → /messages
cyberlux_drop_box drop-box — / → /drop-box
cyberlux_easter_eggs easter-eggs — / → /easter-eggs
cyberlux_links links — / → /links
cyberlux_red_room red-room — / → /red-room
cyberlux_drops drops — / → /drops
cyberlux_inner_circle inner-circle — / → /inner-circle
cyberlux_comparison comparison — / → /comparison
cyberlux_testimonials testimonials — / → /testimonials
cyberlux_wallets wallets — / → /wallets
cyberlux_support support — / → /support
cyberlux_darknet_atlas darknet-atlas — / → /darknet-atlas
cyberlux_security_analysis security-analysis — / → /security-analysis
cyberlux_mixer mixer — / → /mixer
cyberlux_secret_layer secret-layer — / → /secret-layer
cyberlux_trees trees — / → /trees
cyberlux_presswire presswire — / → /presswire
cyberlux_awards awards — / → /awards
cyberlux_raffle raffle — / → /raffle
cyberlux_game game — / → /game
cyberlux_webring webring — / → /webring
cyberlux_conspiracies conspiracies — / → /conspiracies
cyberlux_sanctuary sanctuary — / → /sanctuary
cyberlux_dashboard dashboard — / → /dashboard
cyberlux_checkout checkout — / → /checkout
cyberlux_vendors vendors — / → /vendors
cyberlux_sign_in sign-in — / → /sign-in
cyberlux_sign_up sign-up — / → /sign-up
cyberlux_account account — / → /account
cyberlux_w Shadow nodes — / → /syndicate, /x → /w/x
1 cyberlux Hub — full storefront
cyberlux_forum forum — / → /forum
cyberlux_exchange exchange — / → /exchange
cyberlux_wiki Wiki — / → /hidden-wiki
cyberlux_market market — / → /market
cyberlux_barter barter — / → /barter
cyberlux_chatter chatter — / → /chatter
cyberlux_search search — / → /search
cyberlux_syndicate syndicate — / → /syndicate
cyberlux_arb_academy arb-academy — / → /arb-academy
cyberlux_reviews reviews — / → /reviews
cyberlux_trust trust — / → /trust
cyberlux_vault vault — / → /vault
cyberlux_messages messages — / → /messages
cyberlux_drop_box drop-box — / → /drop-box
cyberlux_easter_eggs easter-eggs — / → /easter-eggs
cyberlux_links links — / → /links
cyberlux_red_room red-room — / → /red-room
cyberlux_drops drops — / → /drops
cyberlux_inner_circle inner-circle — / → /inner-circle
cyberlux_comparison comparison — / → /comparison
cyberlux_testimonials testimonials — / → /testimonials
cyberlux_wallets wallets — / → /wallets
cyberlux_support support — / → /support
cyberlux_darknet_atlas darknet-atlas — / → /darknet-atlas
cyberlux_security_analysis security-analysis — / → /security-analysis
cyberlux_mixer mixer — / → /mixer
cyberlux_secret_layer secret-layer — / → /secret-layer
cyberlux_trees trees — / → /trees
cyberlux_presswire presswire — / → /presswire
cyberlux_awards awards — / → /awards
cyberlux_raffle raffle — / → /raffle
cyberlux_game game — / → /game
cyberlux_webring webring — / → /webring
cyberlux_conspiracies conspiracies — / → /conspiracies
cyberlux_sanctuary sanctuary — / → /sanctuary
cyberlux_dashboard dashboard — / → /dashboard
cyberlux_checkout checkout — / → /checkout
cyberlux_vendors vendors — / → /vendors
cyberlux_sign_in sign-in — / → /sign-in
cyberlux_sign_up sign-up — / → /sign-up
cyberlux_account account — / → /account
cyberlux_w Shadow nodes — / → /syndicate, /x → /w/x

View File

@@ -1 +1 @@
8080 8122 8080 8080

View File

@@ -1,43 +1 @@
cyberlux cyberlux
cyberlux_forum
cyberlux_exchange
cyberlux_wiki
cyberlux_market
cyberlux_barter
cyberlux_chatter
cyberlux_search
cyberlux_syndicate
cyberlux_arb_academy
cyberlux_reviews
cyberlux_trust
cyberlux_vault
cyberlux_messages
cyberlux_drop_box
cyberlux_easter_eggs
cyberlux_links
cyberlux_red_room
cyberlux_drops
cyberlux_inner_circle
cyberlux_comparison
cyberlux_testimonials
cyberlux_wallets
cyberlux_support
cyberlux_darknet_atlas
cyberlux_security_analysis
cyberlux_mixer
cyberlux_secret_layer
cyberlux_trees
cyberlux_presswire
cyberlux_awards
cyberlux_raffle
cyberlux_game
cyberlux_webring
cyberlux_conspiracies
cyberlux_sanctuary
cyberlux_dashboard
cyberlux_checkout
cyberlux_vendors
cyberlux_sign_in
cyberlux_sign_up
cyberlux_account
cyberlux_w

View File

@@ -43,7 +43,7 @@ start_tor() {
node "${REPO}/scripts/generate-onion-config.cjs" node "${REPO}/scripts/generate-onion-config.cjs"
mkdir -p /etc/tor /etc/nginx/sites-available /etc/nginx/sites-enabled mkdir -p /etc/tor /etc/nginx/conf.d /etc/nginx/sites-available /etc/nginx/sites-enabled
touch "${TORRC}" touch "${TORRC}"
tor_changed=0 tor_changed=0
@@ -63,10 +63,10 @@ if ! grep -qF "${TOR_INCLUDE}" "${TORRC}"; then
tor_changed=1 tor_changed=1
fi fi
if install_if_changed "${REPO}/nginx/cyberlux-server-common.inc" /etc/nginx/cyberlux-server-common.inc 0644; then if install_if_changed "${REPO}/nginx/cyberlux-ddos-zones.conf" /etc/nginx/conf.d/cyberlux-ddos-zones.conf 0644; then
nginx_changed=1 nginx_changed=1
fi fi
if install_if_changed "${REPO}/nginx/cyberlux-onion-servers.inc" /etc/nginx/cyberlux-onion-servers.inc 0644; then if install_if_changed "${REPO}/nginx/cyberlux-server-common.inc" /etc/nginx/cyberlux-server-common.inc 0644; then
nginx_changed=1 nginx_changed=1
fi fi
if install_if_changed "${REPO}/nginx.example.conf" /etc/nginx/sites-available/cyberlux-onion 0644; then if install_if_changed "${REPO}/nginx.example.conf" /etc/nginx/sites-available/cyberlux-onion 0644; then

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Install a mkp224o-generated v3 key directory into the CyberLux HiddenServiceDir and restart Tor.
# Run with sudo after vanity-onion-mine.sh has produced a folder named *.onion under the output path.
#
# sudo bash scripts/install-vanity-onion-key.sh /path/to/var/vanity-mine
# sudo bash scripts/install-vanity-onion-key.sh /path/to/specificXXXX.onion
#
# If the path is a parent with several *.onion children, one is chosen automatically
# (default: first in lexicographic order). Override with:
# CYBERLUX_VANITY_PICK=latest — use the directory with the newest mtime
# CYBERLUX_VANITY_PICK=first — same as default (first sorted AZ)
# Or pass a direct path to a single ... .onion folder.
#
set -euo pipefail
[[ "${EUID}" -eq 0 ]] || { echo "Run as root (sudo)."; exit 1; }
REPO="$(cd "$(dirname "$0")/.." && pwd)"
TOR_DIRS_FILE="${REPO}/scripts/generated/tor-dirs.txt"
ARG="${1:-}"
if [[ -z "${ARG}" ]]; then
echo "Usage: sudo bash $0 <vanity_out_dir|path_to_name.onion_dir>"
exit 1
fi
# If ARG is relative, prefer REPO/ARG (so `var/vanity-mine` works from any cwd when using this repo).
if [[ "${ARG}" != /* && -d "${REPO}/${ARG}" ]]; then
ARG="${REPO}/${ARG}"
fi
if [[ ! -f "${TOR_DIRS_FILE}" ]]; then
echo "Missing ${TOR_DIRS_FILE}. Run: cd ${REPO} && node scripts/generate-onion-config.cjs"
exit 1
fi
# Resolve single torDir (CyberLux uses "cyberlux" in onion-nodes.json)
TOR_DIR_NAME="$(head -1 "${TOR_DIRS_FILE}" | tr -d '\r\n')"
if [[ -z "${TOR_DIR_NAME}" ]]; then
echo "Empty ${TOR_DIRS_FILE}"
exit 1
fi
HS_ROOT="/var/lib/tor/${TOR_DIR_NAME}"
KEY_SRC=""
if [[ -d "${ARG}" ]]; then
if [[ -f "${ARG}/hs_ed25519_secret_key" ]]; then
KEY_SRC="${ARG}"
else
# Pick a *.onion child (one or many)
mapfile -t kids < <(find "${ARG}" -maxdepth 1 -type d -name '*.onion' 2>/dev/null | sort)
if [[ "${#kids[@]}" -eq 1 ]]; then
KEY_SRC="${kids[0]}"
elif [[ "${#kids[@]}" -eq 0 ]]; then
echo "No subdirectory matching *.onion under ${ARG} and no key at top level."
exit 1
else
pick="${CYBERLUX_VANITY_PICK:-first}"
case "${pick}" in
first)
KEY_SRC="${kids[0]}"
echo "[*] ${#kids[@]} *.onion key dirs; using first (lexicographic): ${KEY_SRC}"
;;
latest|newest)
latest=""
latest_m=0
for d in "${kids[@]}"; do
m="$(stat -c %Y "${d}" 2>/dev/null || echo 0)"
if (( m >= latest_m )); then
latest_m="${m}"
latest="${d}"
fi
done
KEY_SRC="${latest}"
echo "[*] ${#kids[@]} *.onion key dirs; using latest mtime: ${KEY_SRC}"
;;
*)
echo "Unknown CYBERLUX_VANITY_PICK=${pick} (use first or latest)."
exit 1
;;
esac
fi
fi
else
echo "Not a directory: ${ARG}"
exit 1
fi
if [[ ! -f "${KEY_SRC}/hs_ed25519_secret_key" ]]; then
echo "No hs_ed25519_secret_key in ${KEY_SRC}"
exit 1
fi
TOR_USER=""
for u in debian-tor tor _tor; do
if id -u "${u}" &>/dev/null; then
TOR_USER="${u}"
break
fi
done
if [[ -z "${TOR_USER}" ]]; then
echo "No Tor system user (tried debian-tor, tor, _tor). Install Tor."
exit 1
fi
TS="$(date +%s)"
if [[ -d "${HS_ROOT}" ]]; then
echo "[*] Preserving current service dir as ${HS_ROOT}.stale.${TS}"
mv "${HS_ROOT}" "${HS_ROOT}.stale.${TS}"
fi
mkdir -p "${HS_ROOT}"
shopt -s dotglob nullglob
for item in "${KEY_SRC}"/*; do
base="$(basename "${item}")"
[[ "${base}" == "." || "${base}" == ".." ]] && continue
cp -a "${item}" "${HS_ROOT}/"
done
shopt -u dotglob nullglob
chown -R "${TOR_USER}:${TOR_USER}" "${HS_ROOT}"
chmod -R u+rwX,og-rwx "${HS_ROOT}"
chmod 0700 "${HS_ROOT}"
restart_tor() {
systemctl restart tor@default.service 2>/dev/null || systemctl restart tor
}
echo "[*] Installed key material into ${HS_ROOT} (onion: $(tr -d '\n' < "${HS_ROOT}/hostname" 2>/dev/null || echo '?'))"
restart_tor
if [[ -f "${REPO}/scripts/export-onion-urls.sh" ]]; then
bash "${REPO}/scripts/export-onion-urls.sh" || true
fi
if [[ -f "${REPO}/scripts/backup-onion-keys.sh" ]]; then
bash "${REPO}/scripts/backup-onion-keys.sh" || true
fi
echo "[*] Done. Open: http://$(tr -d '\n' < "${HS_ROOT}/hostname") (in Tor Browser)"
echo "[*] Old keys (if any): ${HS_ROOT}.stale.${TS}"

59
scripts/mkp224o-build.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Build cathugger/mkp224o (Tor v3 vanity .onion key generator) into vendor/mkp224o/mkp224o.
# Requires: git, make, gcc or clang, autoconf, pkg-config, libsodium-dev (Debian: libsodium-dev)
#
# bash scripts/mkp224o-build.sh
#
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
VEND="${REPO}/vendor/mkp224o"
BIN="${VEND}/mkp224o"
if [[ -x "${BIN}" ]]; then
echo "Already built: ${BIN}"
exit 0
fi
if ! command -v git >/dev/null 2>&1; then
echo "Install git and retry."
exit 1
fi
mkdir -p "${REPO}/vendor"
if [[ ! -d "${VEND}/.git" ]]; then
echo "[*] Cloning mkp224o..."
git clone --depth 1 https://github.com/cathugger/mkp224o.git "${VEND}"
fi
cd "${VEND}"
if [[ ! -f "./configure" ]]; then
if [[ -f "./autogen.sh" ]]; then
echo "[*] autogen.sh..."
./autogen.sh
else
echo "No configure/autogen in ${VEND}"
exit 1
fi
fi
if [[ ! -f "./Makefile" ]]; then
echo "[*] configure..."
# Prefer AMD64 fast curve implementation when available
if ./configure --help 2>/dev/null | grep -q 'enable-amd64-64-24k'; then
./configure --enable-amd64-64-24k
else
./configure
fi
fi
echo "[*] make -j \$(nproc)..."
make -j"$(nproc)"
if [[ ! -x "${BIN}" ]]; then
echo "Build failed: ${BIN} not executable"
exit 1
fi
echo "Built: ${BIN}"
echo "Mine a prefix: bash ${REPO}/scripts/vanity-onion-mine.sh <prefix>"

View File

@@ -1,47 +1,5 @@
{ {
"nodes": [ "nodes": [
{ "torDir": "cyberlux", "port": 8080, "kind": "hub" }, { "torDir": "cyberlux", "port": 8080, "kind": "hub" }
{ "torDir": "cyberlux_forum", "port": 8081, "kind": "dedicated", "entry": "forum", "path": "/forum" },
{ "torDir": "cyberlux_exchange", "port": 8082, "kind": "dedicated", "entry": "exchange", "path": "/exchange" },
{ "torDir": "cyberlux_wiki", "port": 8083, "kind": "wiki" },
{ "torDir": "cyberlux_market", "port": 8084, "kind": "dedicated", "entry": "market", "path": "/market" },
{ "torDir": "cyberlux_barter", "port": 8085, "kind": "dedicated", "entry": "barter", "path": "/barter" },
{ "torDir": "cyberlux_chatter", "port": 8086, "kind": "dedicated", "entry": "chatter", "path": "/chatter" },
{ "torDir": "cyberlux_search", "port": 8087, "kind": "dedicated", "entry": "search", "path": "/search" },
{ "torDir": "cyberlux_syndicate", "port": 8088, "kind": "dedicated", "entry": "syndicate", "path": "/syndicate" },
{ "torDir": "cyberlux_arb_academy", "port": 8089, "kind": "dedicated", "entry": "arb-academy", "path": "/arb-academy" },
{ "torDir": "cyberlux_reviews", "port": 8090, "kind": "dedicated", "entry": "reviews", "path": "/reviews" },
{ "torDir": "cyberlux_trust", "port": 8091, "kind": "dedicated", "entry": "trust", "path": "/trust" },
{ "torDir": "cyberlux_vault", "port": 8092, "kind": "dedicated", "entry": "vault", "path": "/vault" },
{ "torDir": "cyberlux_messages", "port": 8093, "kind": "dedicated", "entry": "messages", "path": "/messages" },
{ "torDir": "cyberlux_drop_box", "port": 8094, "kind": "dedicated", "entry": "drop-box", "path": "/drop-box" },
{ "torDir": "cyberlux_easter_eggs", "port": 8095, "kind": "dedicated", "entry": "easter-eggs", "path": "/easter-eggs" },
{ "torDir": "cyberlux_links", "port": 8096, "kind": "dedicated", "entry": "links", "path": "/links" },
{ "torDir": "cyberlux_red_room", "port": 8097, "kind": "dedicated", "entry": "red-room", "path": "/red-room" },
{ "torDir": "cyberlux_drops", "port": 8098, "kind": "dedicated", "entry": "drops", "path": "/drops" },
{ "torDir": "cyberlux_inner_circle", "port": 8099, "kind": "dedicated", "entry": "inner-circle", "path": "/inner-circle" },
{ "torDir": "cyberlux_comparison", "port": 8100, "kind": "dedicated", "entry": "comparison", "path": "/comparison" },
{ "torDir": "cyberlux_testimonials", "port": 8101, "kind": "dedicated", "entry": "testimonials", "path": "/testimonials" },
{ "torDir": "cyberlux_wallets", "port": 8102, "kind": "dedicated", "entry": "wallets", "path": "/wallets" },
{ "torDir": "cyberlux_support", "port": 8103, "kind": "dedicated", "entry": "support", "path": "/support" },
{ "torDir": "cyberlux_darknet_atlas", "port": 8104, "kind": "dedicated", "entry": "darknet-atlas", "path": "/darknet-atlas" },
{ "torDir": "cyberlux_security_analysis", "port": 8105, "kind": "dedicated", "entry": "security-analysis", "path": "/security-analysis" },
{ "torDir": "cyberlux_mixer", "port": 8106, "kind": "dedicated", "entry": "mixer", "path": "/mixer" },
{ "torDir": "cyberlux_secret_layer", "port": 8107, "kind": "dedicated", "entry": "secret-layer", "path": "/secret-layer" },
{ "torDir": "cyberlux_trees", "port": 8108, "kind": "dedicated", "entry": "trees", "path": "/trees" },
{ "torDir": "cyberlux_presswire", "port": 8109, "kind": "dedicated", "entry": "presswire", "path": "/presswire" },
{ "torDir": "cyberlux_awards", "port": 8110, "kind": "dedicated", "entry": "awards", "path": "/awards" },
{ "torDir": "cyberlux_raffle", "port": 8111, "kind": "dedicated", "entry": "raffle", "path": "/raffle" },
{ "torDir": "cyberlux_game", "port": 8112, "kind": "dedicated", "entry": "game", "path": "/game" },
{ "torDir": "cyberlux_webring", "port": 8113, "kind": "dedicated", "entry": "webring", "path": "/webring" },
{ "torDir": "cyberlux_conspiracies", "port": 8114, "kind": "dedicated", "entry": "conspiracies", "path": "/conspiracies" },
{ "torDir": "cyberlux_sanctuary", "port": 8115, "kind": "dedicated", "entry": "sanctuary", "path": "/sanctuary" },
{ "torDir": "cyberlux_dashboard", "port": 8116, "kind": "dedicated", "entry": "dashboard", "path": "/dashboard" },
{ "torDir": "cyberlux_checkout", "port": 8117, "kind": "dedicated", "entry": "checkout", "path": "/checkout" },
{ "torDir": "cyberlux_vendors", "port": 8118, "kind": "dedicated", "entry": "vendors", "path": "/vendors" },
{ "torDir": "cyberlux_sign_in", "port": 8119, "kind": "dedicated", "entry": "sign-in", "path": "/sign-in" },
{ "torDir": "cyberlux_sign_up", "port": 8120, "kind": "dedicated", "entry": "sign-up", "path": "/sign-up" },
{ "torDir": "cyberlux_account", "port": 8121, "kind": "dedicated", "entry": "account", "path": "/account" },
{ "torDir": "cyberlux_w", "port": 8122, "kind": "w" }
] ]
} }

View File

@@ -124,7 +124,7 @@ async function main() {
console.log(`${bad} loopback vhost(s) did not return OK HTTP — ensure: systemctl status nginx tor; systemctl status cyberlux`); console.log(`${bad} loopback vhost(s) did not return OK HTTP — ensure: systemctl status nginx tor; systemctl status cyberlux`);
process.exit(1); process.exit(1);
} }
console.log("✓ All loopback nginx vhosts responded (Tor .onion forwarding uses these ports)."); console.log("✓ All configured loopback nginx vhosts responded (Tor .onion forwarding uses these ports).");
console.log(" To test via the Tor network use Tor Browser (not curl from clearnet)."); console.log(" To test via the Tor network use Tor Browser (not curl from clearnet).");
process.exit(0); process.exit(0);
} }

50
scripts/vanity-onion-mine.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Brute-force a Tor v3 .onion whose address starts with PREFIX (base32: az, 27).
# Longer prefixes take exponentially more CPU time. Run on a strong machine; be patient.
#
# bash scripts/mkp224o-build.sh
# bash scripts/vanity-onion-mine.sh cyberl
# sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine
#
# Env:
# CYBERLUX_MKP224O — path to mkp224o binary (default: vendor/mkp224o/mkp224o)
# CYBERLUX_VANITY_OUT — output directory (default: repo var/vanity-mine)
# CYBERLUX_MKP224O_DOCKER=1 — use Docker image instead of local binary
#
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
PREFIX="${1:-}"
if [[ -z "${PREFIX}" ]]; then
echo "Usage: $0 <prefix>"
echo "Example: $0 cyberl (8 chars: expect long run; 46 is more practical on one PC)"
echo "Onion v3 hostnames use base32: letters az and digits 27 only (no 0,1,8,9)."
exit 1
fi
if ! [[ "${PREFIX}" =~ ^[a-z2-7]+$ ]]; then
echo "Invalid prefix: use only az and 27 (Tor onion base32)."
exit 1
fi
OUT_DIR="${CYBERLUX_VANITY_OUT:-${REPO}/var/vanity-mine}"
mkdir -p "${OUT_DIR}"
if [[ "${CYBERLUX_MKP224O_DOCKER:-0}" == "1" ]]; then
echo "[*] Mining with Docker (ghcr.io/cathugger/mkp224o:master) → ${OUT_DIR}"
# -s = print stats every 10s; last arg is the base32 filter
exec docker run --rm -it -v "${OUT_DIR}:/keys" ghcr.io/cathugger/mkp224o:master -d /keys -s "${PREFIX}"
fi
MKP="${CYBERLUX_MKP224O:-${REPO}/vendor/mkp224o/mkp224o}"
if [[ ! -x "${MKP}" ]]; then
echo "Missing ${MKP}. Build first:"
echo " bash ${REPO}/scripts/mkp224o-build.sh"
echo "Or set CYBERLUX_MKP224O_DOCKER=1 and re-run this script."
exit 1
fi
echo "[*] Mining (Ctrl+C to stop). Output: ${OUT_DIR}/"
echo "[*] Statistics (-s) enabled. First match appears as a new directory ending in .onion"
"${MKP}" -d "${OUT_DIR}" -s "${PREFIX}"

View File

@@ -20,7 +20,7 @@ try {
run("TypeScript (tsc --noEmit)", "npx tsc --noEmit"); run("TypeScript (tsc --noEmit)", "npx tsc --noEmit");
run( run(
"bash syntax (start.sh, install, backup, restore, systemd, health)", "bash syntax (start.sh, install, backup, restore, systemd, health)",
"bash -n start.sh && bash -n scripts/install-tor-onion.sh && bash -n scripts/install-systemd.sh && bash -n scripts/health-check-stack.sh && bash -n scripts/list-onion-urls.sh && bash -n scripts/export-onion-urls.sh && bash -n scripts/diagnose-onion-stack.sh && bash -n scripts/backup-onion-keys.sh && bash -n scripts/restore-onion-keys.sh", "bash -n start.sh && bash -n scripts/install-tor-onion.sh && bash -n scripts/install-systemd.sh && bash -n scripts/health-check-stack.sh && bash -n scripts/list-onion-urls.sh && bash -n scripts/export-onion-urls.sh && bash -n scripts/diagnose-onion-stack.sh && bash -n scripts/backup-onion-keys.sh && bash -n scripts/restore-onion-keys.sh && bash -n scripts/mkp224o-build.sh && bash -n scripts/vanity-onion-mine.sh && bash -n scripts/install-vanity-onion-key.sh",
); );
run("node syntax (onion-status.cjs)", "node --check scripts/onion-status.cjs"); run("node syntax (onion-status.cjs)", "node --check scripts/onion-status.cjs");
run("Next.js production build", "npm run build"); run("Next.js production build", "npm run build");

View File

@@ -48,7 +48,7 @@ fi
read -r ONION_PORT_MIN ONION_PORT_MAX < "${RANGE_FILE}" || true read -r ONION_PORT_MIN ONION_PORT_MAX < "${RANGE_FILE}" || true
ONION_PORT_MIN="${ONION_PORT_MIN:-8080}" ONION_PORT_MIN="${ONION_PORT_MIN:-8080}"
ONION_PORT_MAX="${ONION_PORT_MAX:-8122}" ONION_PORT_MAX="${ONION_PORT_MAX:-8080}"
mapfile -t CYBERLUX_TOR_DIRS < "${TOR_DIRS_FILE}" mapfile -t CYBERLUX_TOR_DIRS < "${TOR_DIRS_FILE}"
@@ -81,14 +81,14 @@ wait_for_all_hostnames() {
done done
ready="$(count_ready_hostnames)" ready="$(count_ready_hostnames)"
echo "[!] Timeout waiting for all onion hostnames (${ready}/${total} ready)." >&2 echo "[!] Timeout waiting for onion hostname files (${ready}/${total} ready)." >&2
return 1 return 1
} }
print_onion_banner() { print_onion_banner() {
echo "" echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " CYBERLUX — copy these .onion URLs (Tor Browser, http:// only)" echo " CYBERLUX — copy this .onion URL (Tor Browser, http:// only)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local any=0 local any=0
local ready=0 local ready=0

View File

@@ -5,215 +5,5 @@
HiddenServiceDir /var/lib/tor/cyberlux HiddenServiceDir /var/lib/tor/cyberlux
HiddenServicePort 80 127.0.0.1:8080 HiddenServicePort 80 127.0.0.1:8080
HiddenServiceEnableIntroDoSDefense 1 HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25 HiddenServiceEnableIntroDoSRatePerSec 12
HiddenServiceEnableIntroDoSBurstPerSec 200 HiddenServiceEnableIntroDoSBurstPerSec 80
HiddenServiceDir /var/lib/tor/cyberlux_forum
HiddenServicePort 80 127.0.0.1:8081
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_exchange
HiddenServicePort 80 127.0.0.1:8082
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_wiki
HiddenServicePort 80 127.0.0.1:8083
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_market
HiddenServicePort 80 127.0.0.1:8084
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_barter
HiddenServicePort 80 127.0.0.1:8085
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_chatter
HiddenServicePort 80 127.0.0.1:8086
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_search
HiddenServicePort 80 127.0.0.1:8087
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_syndicate
HiddenServicePort 80 127.0.0.1:8088
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_arb_academy
HiddenServicePort 80 127.0.0.1:8089
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_reviews
HiddenServicePort 80 127.0.0.1:8090
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_trust
HiddenServicePort 80 127.0.0.1:8091
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_vault
HiddenServicePort 80 127.0.0.1:8092
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_messages
HiddenServicePort 80 127.0.0.1:8093
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_drop_box
HiddenServicePort 80 127.0.0.1:8094
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_easter_eggs
HiddenServicePort 80 127.0.0.1:8095
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_links
HiddenServicePort 80 127.0.0.1:8096
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_red_room
HiddenServicePort 80 127.0.0.1:8097
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_drops
HiddenServicePort 80 127.0.0.1:8098
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_inner_circle
HiddenServicePort 80 127.0.0.1:8099
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_comparison
HiddenServicePort 80 127.0.0.1:8100
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_testimonials
HiddenServicePort 80 127.0.0.1:8101
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_wallets
HiddenServicePort 80 127.0.0.1:8102
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_support
HiddenServicePort 80 127.0.0.1:8103
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_darknet_atlas
HiddenServicePort 80 127.0.0.1:8104
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_security_analysis
HiddenServicePort 80 127.0.0.1:8105
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_mixer
HiddenServicePort 80 127.0.0.1:8106
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_secret_layer
HiddenServicePort 80 127.0.0.1:8107
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_trees
HiddenServicePort 80 127.0.0.1:8108
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_presswire
HiddenServicePort 80 127.0.0.1:8109
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_awards
HiddenServicePort 80 127.0.0.1:8110
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_raffle
HiddenServicePort 80 127.0.0.1:8111
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_game
HiddenServicePort 80 127.0.0.1:8112
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_webring
HiddenServicePort 80 127.0.0.1:8113
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_conspiracies
HiddenServicePort 80 127.0.0.1:8114
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_sanctuary
HiddenServicePort 80 127.0.0.1:8115
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_dashboard
HiddenServicePort 80 127.0.0.1:8116
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_checkout
HiddenServicePort 80 127.0.0.1:8117
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_vendors
HiddenServicePort 80 127.0.0.1:8118
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_sign_in
HiddenServicePort 80 127.0.0.1:8119
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_sign_up
HiddenServicePort 80 127.0.0.1:8120
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_account
HiddenServicePort 80 127.0.0.1:8121
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HiddenServiceDir /var/lib/tor/cyberlux_w
HiddenServicePort 80 127.0.0.1:8122
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200