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+.

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:

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:

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:

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

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, Discord interactions, Discord application commands, http-proxy-middleware.

Description
The proxy for Dungeons-Ground
Readme 99 KiB
Languages
JavaScript 93.4%
HTML 5.9%
Dockerfile 0.7%