Replace remote with local repo
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
18
DEPLOY.md
18
DEPLOY.md
@@ -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:8080–8122` (see `scripts/onion-nodes.json`); Tor forwards port 80 on each onion to the matching loopback port. **Next.js binds only `127.0.0.1:3000`** — never expose 3000 or the nginx loopback ports to the public internet.
|
This app is designed to run **behind nginx on loopback**, with **one Tor v3 hidden service for the whole site**. Nginx listens on `127.0.0.1:8080` (see `scripts/onion-nodes.json`); Tor forwards port 80 on the onion to that loopback port. **Next.js binds only `127.0.0.1:3000`** — never expose 3000 or the nginx loopback port to the public internet.
|
||||||
|
|
||||||
Onion hostnames are **created locally** when Tor first starts; there are no fixed `.onion` URLs in the repo. Back up `/var/lib/tor/*/hs_ed25519_secret_key` (the install flow uses `scripts/backup-onion-keys.sh`).
|
Onion hostnames are **created locally** when Tor first starts; there are no fixed `.onion` URLs in the repo. Back up `/var/lib/tor/cyberlux/hs_ed25519_secret_key` (the install flow uses `scripts/backup-onion-keys.sh`). A vanity prefix such as `cyberlux...onion` requires generating or importing a matching Tor v3 hidden-service key.
|
||||||
|
|
||||||
## One-time server setup (Debian/Ubuntu-style)
|
## 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
|
||||||
|
|||||||
130
ONION-URLS.md
130
ONION-URLS.md
@@ -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: **a–z** and **2–7** only (no `0`, `1`, `8`, `9`). Any other character in the filter is invalid.
|
||||||
|
|
||||||
|
**Time:** Shorter prefixes finish sooner; each extra character is much harder. Treat long targets (e.g. full word) as a **long** CPU run or use many cores / a batch-capable build per mkp224o’s `OPTIMISATION.txt`.
|
||||||
|
|
||||||
|
### 1) Build the miner (once)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu deps (as root)
|
||||||
|
sudo apt install -y git gcc make autoconf pkg-config libc6-dev libsodium-dev
|
||||||
|
|
||||||
|
cd /path/to/cyberlux
|
||||||
|
bash scripts/mkp224o-build.sh
|
||||||
|
# produces: vendor/mkp224o/mkp224o
|
||||||
|
```
|
||||||
|
|
||||||
|
`vendor/mkp224o` is gitignored; nothing is committed except these scripts.
|
||||||
|
|
||||||
|
**Docker alternative (no local compile):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CYBERLUX_MKP224O_DOCKER=1
|
||||||
|
export CYBERLUX_VANITY_OUT="/path/to/cyberlux/var/vanity-mine"
|
||||||
|
mkdir -p "$CYBERLUX_VANITY_OUT"
|
||||||
|
bash scripts/vanity-onion-mine.sh yourprefix
|
||||||
|
```
|
||||||
|
|
||||||
|
(Requires Docker; uses `ghcr.io/cathugger/mkp224o:master`.)
|
||||||
|
|
||||||
|
### 2) Mine a prefix (runs until a match appears)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/vanity-onion-mine.sh cyberl
|
||||||
|
# Output directory: var/vanity-mine/ (default) — gitignored
|
||||||
|
```
|
||||||
|
|
||||||
|
You get a new subdirectory named like `cyberlXXXXXXXX.onion` containing `hs_ed25519_secret_key` and `hostname`. **Stop the miner** when you have a directory you are happy with if it created more than one.
|
||||||
|
|
||||||
|
### 3) Install the key for CyberLux and restart Tor
|
||||||
|
|
||||||
|
`HiddenServiceDir` in this project is `/var/lib/tor/cyberlux` (from `onion-nodes.json`). The install script copies the **files** from the mkp224o folder into that directory, fixes ownership, and restarts Tor.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If var/vanity-mine has several *.onion folders, the script picks one for you
|
||||||
|
# (default: first in A–Z order). Newest mtime: CYBERLUX_VANITY_PICK=latest
|
||||||
|
sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine
|
||||||
|
sudo env CYBERLUX_VANITY_PICK=latest bash scripts/install-vanity-onion-key.sh var/vanity-mine
|
||||||
|
|
||||||
|
# Or point at one directory explicitly:
|
||||||
|
sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine/cyberlxxxxx....onion
|
||||||
|
```
|
||||||
|
|
||||||
|
The previous service directory is moved to `/var/lib/tor/cyberlux.stale.<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:8080–8122` 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.
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -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:8080–8122 (one vhost per .onion, host header → X-Cyberlux-Node)
|
nginx 127.0.0.1:8080 (single onion vhost)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Next.js 127.0.0.1:3000 (single instance, proxy.ts routes by header)
|
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:8080–8122` (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 8080–8122 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
193
app/academy/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
app/api/admin/credit/route.ts
Normal file
25
app/api/admin/credit/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
13
app/api/admin/ledger/route.ts
Normal file
13
app/api/admin/ledger/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
12
app/api/admin/raffle/route.ts
Normal file
12
app/api/admin/raffle/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
95
app/api/btcpay/webhook/route.ts
Normal file
95
app/api/btcpay/webhook/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
24
app/api/credits/balance/route.ts
Normal file
24
app/api/credits/balance/route.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
90
app/api/credits/claim/route.ts
Normal file
90
app/api/credits/claim/route.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
91
app/api/raffle/buy/route.ts
Normal file
91
app/api/raffle/buy/route.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
44
app/api/tools/unlock/route.ts
Normal file
44
app/api/tools/unlock/route.ts
Normal 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()),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
147
app/codex/page.tsx
Normal 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
109
app/directory/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
197
app/hosting/page.tsx
Normal 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'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
267
app/labs/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
150
app/nexus/page.tsx
Normal 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
134
app/oracle/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
334
app/tools/page.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
156
components/RaffleCountdownBanner.tsx
Normal file
156
components/RaffleCountdownBanner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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." };
|
||||||
|
|||||||
@@ -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: "🚀" },
|
||||||
|
|||||||
@@ -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
218
lib/serverLedger.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
136
lib/toolsCatalog.ts
Normal 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",
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
|
|||||||
12
nginx/cyberlux-ddos-zones.conf
Normal file
12
nginx/cyberlux-ddos-zones.conf
Normal 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
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 +1 @@
|
|||||||
8080 8122
|
8080 8080
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
140
scripts/install-vanity-onion-key.sh
Executable file
140
scripts/install-vanity-onion-key.sh
Executable 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 A–Z)
|
||||||
|
# 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
59
scripts/mkp224o-build.sh
Executable 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>"
|
||||||
@@ -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" }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
50
scripts/vanity-onion-mine.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Brute-force a Tor v3 .onion whose address starts with PREFIX (base32: a–z, 2–7).
|
||||||
|
# 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; 4–6 is more practical on one PC)"
|
||||||
|
echo "Onion v3 hostnames use base32: letters a–z and digits 2–7 only (no 0,1,8,9)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "${PREFIX}" =~ ^[a-z2-7]+$ ]]; then
|
||||||
|
echo "Invalid prefix: use only a–z and 2–7 (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}"
|
||||||
@@ -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");
|
||||||
|
|||||||
6
start.sh
6
start.sh
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user