120 lines
12 KiB
Markdown
120 lines
12 KiB
Markdown
# Dungeons & Ground · Access gateway
|
||
|
||
A private-alpha reverse proxy with an access page matching Dungeons-Ground’s dark grid, lime accents, Unbounded/Manrope/DM Mono typography, and D&G mark. All implementation lives in **Dungeons-Proxy**; Dungeons-Ground was used as a read-only reference.
|
||
|
||
Users open the Telegram or Discord bot and press **Get access**. Configured administrators receive the requester's numeric user ID, selected profile details, and **Approve / Deny** buttons. Approval sends a randomly generated numeric access code to the requester’s DM. The same number can be used on multiple devices for **15 days** by default. Reusing it does not extend its expiry. Once it expires, all associated browser sessions stop working and the user must request a new number and approval.
|
||
|
||
The gateway forwards the configured Dungeons-Ground site on the same public origin after entry. It protects HTML, API routes, static assets, uploads, and WebSocket upgrades. Deep links return to their original route after entry. Existing application login remains separate from the alpha access gate.
|
||
|
||
## Local start
|
||
|
||
Use Node.js **22.15+ on the Node 22 line** (the `.nvmrc` selects Node 22), Node 24, or Node 26+.
|
||
|
||
```sh
|
||
nvm use
|
||
npm ci
|
||
cp .env.example .env
|
||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
```
|
||
|
||
Paste the generated value into `SESSION_SECRET` in `.env`. Set `UPSTREAM_URL` to the Dungeons-Ground origin, set `PUBLIC_URL` to the gateway's public origin, and configure one or both bots below. Start the existing Dungeons-Ground site separately, then:
|
||
|
||
```sh
|
||
npm start
|
||
```
|
||
|
||
With the default local URLs, open `http://localhost:8080`; the upstream is `http://127.0.0.1:3000`. `npm run dev` restarts on source changes. Environment variables supplied by a host override `.env`. The server reads `.env` from the directory in which it starts.
|
||
|
||
For a visual preview, development mode may run with both tokens empty. It displays an honest unavailable message in place of bot links; there is no built-in bypass number. Production requires at least one configured provider and an HTTPS `PUBLIC_URL`.
|
||
|
||
## Telegram setup
|
||
|
||
1. Create a bot with BotFather and put its token in `TELEGRAM_BOT_TOKEN`.
|
||
2. Set `TELEGRAM_BOT_USERNAME` to its username without `@`.
|
||
3. Set `TELEGRAM_ADMIN_IDS` to comma-separated **numeric user IDs**, not usernames or group IDs. Admins receive requests in their private chats.
|
||
4. Every admin must open the bot and send `/start` so the bot can DM them. Requesters do the same using the Telegram button on the gate.
|
||
5. Run the gateway. It uses `getUpdates` long polling, so no Telegram webhook or public callback endpoint is needed. Run only one poller per token. If an existing webhook is configured, remove it before starting this poller.
|
||
|
||
`/start` and `/access` show **Get access** in a private chat. The bot acknowledges button presses, checks the acting admin's actual Telegram ID, and delivers approved numbers privately. The last processed update offset is persisted.
|
||
|
||
## Discord setup
|
||
|
||
1. Create an application and bot in the Discord Developer Portal. Put the bot token, application ID, and application's public key into `DISCORD_BOT_TOKEN`, `DISCORD_APPLICATION_ID`, and `DISCORD_PUBLIC_KEY`.
|
||
2. Put comma-separated numeric admin user IDs into `DISCORD_ADMIN_IDS`. The bot and admins should share the server, and admins and requesters must allow direct messages from it.
|
||
3. Invite the bot to your server with the `bot` and `applications.commands` OAuth scopes. No privileged gateway intents are required; this integration receives signed HTTP interactions.
|
||
4. Set `DISCORD_ACCESS_URL` to your Discord channel link or server invite. This is where the gate sends users to invoke `/access`.
|
||
5. Expose the gateway over HTTPS and set its **Interactions Endpoint URL** to `https://YOUR-GATEWAY/_access/discord`. The server answers Discord's signed verification ping. Never put a separate authentication wall in front of this endpoint.
|
||
6. Run `npm run discord:register` to register `/access`. Optional `DISCORD_GUILD_ID` registers it in one server for immediate availability; otherwise it is global and may need time to propagate. The script only upserts `/access`, preserving unrelated commands.
|
||
|
||
`/access` responds with a **Get access** button. Request and review operations defer their replies while DMs are sent. The endpoint verifies the Ed25519 signature against the original request bytes and rejects stale timestamps. Only configured Discord admin IDs can approve or deny. No bot secrets are exposed to the browser.
|
||
|
||
## Configuration
|
||
|
||
`.env.example` contains every setting with defaults.
|
||
|
||
| Variable | Purpose / default |
|
||
| --- | --- |
|
||
| `UPSTREAM_URL` | Private HTTP(S) origin of the Dungeons-Ground site; no path or credentials |
|
||
| `PUBLIC_URL` | Public gateway origin, used for links, cookies and origin checks |
|
||
| `HOST`, `PORT` | Listen address and port; `0.0.0.0:8080` |
|
||
| `SESSION_SECRET` | Random secret of at least 32 characters; keep stable across restarts |
|
||
| `DATA_FILE` | Durable state file; `./data/access.json` |
|
||
| `CODE_LENGTH` | Numeric code length; 10 digits, configurable from 8–12 |
|
||
| `CODE_TTL_DAYS` | Code validity from approval; **15 days**, configurable from 1–90 |
|
||
| `SESSION_TTL_HOURS` | Browser session maximum; 360 hours, always capped at code expiry |
|
||
| `REQUEST_TTL_HOURS` | How long a pending request can be approved; 24 hours |
|
||
| `REQUEST_COOLDOWN_SECONDS` | Minimum interval between replacement requests; 300 seconds |
|
||
| `LOGIN_ATTEMPTS`, `LOGIN_WINDOW_SECONDS` | Per-IP entry limit; 5 attempts per 900 seconds |
|
||
| `GLOBAL_LOGIN_ATTEMPTS` | Aggregate entry limit per window; 100, including successful entries |
|
||
| `TRUST_PROXY` | Trust a single IP in `X-Forwarded-For`; false by default |
|
||
| `ADMIN_REQUEST_FIELDS` | Optional details shown to admins: `username,display_name,language_code,guild_id,created_at` |
|
||
|
||
Platform, numeric user ID, and request ID always appear in the admin message. Missing optional details show as `—`. Display names and usernames are informational; only configured numeric IDs authorize reviews. These details are collected from the bot interaction, not submitted by the website.
|
||
|
||
## Deployment
|
||
|
||
Run one gateway instance behind an HTTPS edge such as Caddy or nginx. Keep the upstream reachable **only** from the gateway or private network; an independently public upstream can be accessed directly. Configure the upstream application's generated links and authentication callback URLs to use the public gateway origin. The proxy preserves request paths, query strings, methods, bodies, application cookies, and authorization headers; it rewrites upstream-origin redirects and cookie domains for the public gateway. Arbitrary absolute URLs inside upstream HTML/JavaScript are not rewritten.
|
||
|
||
The reserved `/_access` namespace contains the gate, its assets, login/logout endpoints, health endpoint and Discord interactions. Do not cache authenticated proxy responses at the edge. The gateway marks them `private, no-store`. Leave `TRUST_PROXY=false` unless the gateway is reachable exclusively through a trusted edge that **overwrites** `X-Forwarded-For` with one client IP. Comma-separated forwarding chains are deliberately not trusted.
|
||
|
||
For Docker, edit `.env` to set an HTTPS `PUBLIC_URL` and bot credentials, then:
|
||
|
||
```sh
|
||
docker compose up --build -d
|
||
docker compose logs -f gateway
|
||
```
|
||
|
||
Compose binds the gateway to host loopback for your TLS edge and stores data in a named volume. When the upstream is on the Docker host, use `UPSTREAM_URL=http://host.docker.internal:3000`, not `localhost` (which means the gateway container). When it is another container, connect both to the same Docker network and use that service's hostname. `PORT` controls the host binding; the container always listens on 8080. The image runs as an unprivileged user. No live bot credentials are included in this repository.
|
||
|
||
## Persistence and operation
|
||
|
||
- Requests, approvals, hashed codes, hashed session tokens, hashed-IP rate limits, and the Telegram offset persist in an atomically replaced, fsynced JSON file with mode `0600`. Plain access numbers exist only during generation and DM delivery; they are not stored or logged.
|
||
- This is a small private-alpha **single-process** store, capped at 10,000 retained requests and 10,000 active sessions. It is not a distributed database. Use one instance and a durable local volume; do not use autoscaling, clustered workers, or multiple replicas sharing this file. Expired sessions and old request records are pruned periodically; requests are retained for 30 days beyond their last expiry.
|
||
- A `.lock` file prevents simultaneous writers. Normal SIGINT/SIGTERM shutdown releases it. After an abrupt kill or host crash, verify the recorded PID belongs to the stopped gateway before removing the stale lock; never delete a live instance's lock. In Docker, inspect the container state before restarting with a stale lock.
|
||
- Back up the data file and secret together while the gateway is stopped. Corrupt state fails startup instead of silently discarding access controls. Rotating the secret invalidates existing numbers and browser sessions.
|
||
- If a code DM fails, the request stays pending and no code is activated; the admin can press Approve again after the user enables DMs. Undelivered admin notifications can be retried with Get access. Duplicate requests/reviews do not create duplicate approvals. After a crash during delivery, the request returns to pending and the admin should approve it again; an uncertain delivery is never activated automatically.
|
||
- While a number remains valid, pressing Get access points the user to their existing DM. Lost numbers cannot be recovered from hashes: revoke that user's access below, then let them request a fresh approval.
|
||
- Numbers are bearer access credentials and can be entered on multiple devices. Browser cookies are HttpOnly and SameSite=Lax, with Secure enabled for HTTPS. Number entry and logout require the configured same-origin page. Both per-IP and aggregate limits apply to entry attempts, even after a process restart. WebSockets are checked at upgrade and closed within one second of session/code expiry or revocation.
|
||
|
||
To revoke all numbers, pending requests and sessions belonging to a user, stop the gateway first and run:
|
||
|
||
```sh
|
||
npm run sessions:revoke -- telegram 123456789
|
||
# Or: npm run sessions:revoke -- discord 123456789012345678
|
||
```
|
||
|
||
Then restart it. The user may request again after the configured request cooldown. To leave one browser session, send `POST /_access/logout` from the same gateway origin; it clears that session without invalidating the reusable number.
|
||
|
||
`GET /_access/health` reports that the HTTP process is serving requests; it does not certify upstream or bot availability. Polling/transport failures use redacted logs. The page uses Google Fonts, with system font fallbacks if unavailable.
|
||
|
||
## Optional local verification
|
||
|
||
```sh
|
||
npm run check
|
||
npm test
|
||
```
|
||
|
||
The suite covers both bot adapters with mocked provider APIs, administrator authorization, failed DM recovery, duplicate reviews, reusable codes and expiry/renewal, persistent sessions, rate limits, signed Discord interactions, unauthenticated HTTP/WebSocket denial, streaming proxy requests, redirects, cookies, logout, and upstream outages. It requires no external credentials. Live Telegram/Discord delivery still needs an installation with real tokens, configured admins and working DMs.
|
||
|
||
Protocol references: [Telegram Bot API](https://core.telegram.org/bots/api), [Discord interactions](https://docs.discord.com/developers/interactions/receiving-and-responding), [Discord application commands](https://docs.discord.com/developers/interactions/application-commands), [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).
|