mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-20 15:48:33 +02:00
feat: productionize paid modes and PvP capture
This commit is contained in:
+3
-11
@@ -62,17 +62,9 @@ block deadline, so the house edge is a simulation problem. `src/sim/` drives the
|
||||
headless bots; `NETWORK_TUNING` + multi-level depth are the calibrated levers. See
|
||||
[BALANCE_SIM.md](BALANCE_SIM.md) and [MULTI_LEVEL.md](MULTI_LEVEL.md).
|
||||
|
||||
## Regenerating the API reference (autodocs)
|
||||
|
||||
The code is heavily JSDoc-commented. To regenerate a Markdown API reference from those comments:
|
||||
|
||||
```bash
|
||||
cd src && npm install # first time, pulls jsdoc-to-markdown (devDependency)
|
||||
npm run docs:api # writes docs/API/*.md
|
||||
```
|
||||
|
||||
Autodocs cover the **reference** layer (what each function/param is). The **why** — the seams and
|
||||
flows in the pages above — is hand-authored and lives here in `docs/`. Keep both.
|
||||
The code is heavily JSDoc-commented, while the hand-authored pages here explain subsystem seams and
|
||||
operator intent. A docs-only generator is intentionally not part of the production dependency lock;
|
||||
add one in a dedicated documentation toolchain if generated API pages become a release artifact.
|
||||
|
||||
## Conventions worth knowing
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Disposable clone migration gate
|
||||
|
||||
`npm run db:migrate:clone` applies the release's normal ordered migrations to an existing,
|
||||
disposable PostgreSQL clone. It is the migration step for a restored production dump before an
|
||||
immutable release is eligible for activation. It never creates, restores, drops, audits, or
|
||||
activates a database.
|
||||
|
||||
## Hard target contract
|
||||
|
||||
The runner refuses to open a connection unless all of these conditions hold:
|
||||
|
||||
- `DB_HOST` is explicit loopback (`127.0.0.1`, `::1`, or `localhost`) or the standard local Unix
|
||||
socket directory `/run/postgresql` or `/var/run/postgresql`;
|
||||
- `DB_NAME` starts with `monerogue_`, `wownerogue_`, or `wowngeon_` and contains a complete
|
||||
underscore-delimited `clone`, `restore`, or `canary` token;
|
||||
- the name contains no live marker beginning with `prd`, `prod`, `production`, `live`, `mainnet`,
|
||||
`master`, `primary`, `current`, or `active`;
|
||||
- `CLONE_MIGRATION_EXPECT_DATABASE` exactly equals `DB_NAME`;
|
||||
- `CLONE_MIGRATION_CONFIRM` exactly equals `MIGRATE_DISPOSABLE_CLONE:<DB_NAME>`;
|
||||
- `DB_PORT` and `DB_USER` are explicit, `DATABASE_URL` is absent, and the process receives no
|
||||
command-line arguments.
|
||||
|
||||
After connecting, the runner checks PostgreSQL's `current_database()` before any DDL. It then calls
|
||||
the same `DatabaseManager.runMigrations()` used by application startup: lexical filename order,
|
||||
the `schema_migrations` ledger, and one transaction per newly applied file. Finally, it requires the
|
||||
ledger to equal the complete repository migration manifest, in order, with no missing or extra
|
||||
entry. The manifest is discovered from the candidate release and must be contiguous from 001; the
|
||||
current floor is migration 040. A successful current release reports:
|
||||
|
||||
```text
|
||||
clone_migration_status=ok
|
||||
database=monerogue_restore_20260721t220048z_28d513b29b36
|
||||
migration_count=40
|
||||
latest_migration=040_paid_match_entropy_precommit.sql
|
||||
```
|
||||
|
||||
## Invocation
|
||||
|
||||
Create and restore the clone with the reviewed backup procedure first. Keep the target variables in
|
||||
a protected operator-owned environment file and database credentials in a mode-0600 `PGPASSFILE`;
|
||||
do not type a password, connection URL, or target as a command argument. For example, the inherited
|
||||
non-secret target environment is:
|
||||
|
||||
```dotenv
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=monerogue_restore_20260721t220048z_28d513b29b36
|
||||
DB_USER=monerogue
|
||||
PGPASSFILE=/run/wowngeon/clone-migration.pgpass
|
||||
CLONE_MIGRATION_EXPECT_DATABASE=monerogue_restore_20260721t220048z_28d513b29b36
|
||||
CLONE_MIGRATION_CONFIRM=MIGRATE_DISPOSABLE_CLONE:monerogue_restore_20260721t220048z_28d513b29b36
|
||||
```
|
||||
|
||||
Database names use lowercase letters, digits, and underscores only. Normalize a release ID into a
|
||||
separate safe tag rather than copying a hyphenated artifact ID into `DB_NAME`.
|
||||
|
||||
Load that protected environment through the reviewed service/wrapper, change into the extracted
|
||||
candidate's `src` directory, and run only:
|
||||
|
||||
```bash
|
||||
npm run db:migrate:clone
|
||||
```
|
||||
|
||||
Do not append `--` arguments. The runner never logs host, role, password, connection URL, or
|
||||
`PGPASSFILE` contents. `DB_PASSWORD` is supported only for an already-protected inherited service
|
||||
environment and is kept in memory; `PGPASSFILE` is preferred.
|
||||
|
||||
## Required follow-on gates and limits
|
||||
|
||||
Run `scripts/deploy/financial-audit.sql` and the restored-data constraint validation gate against
|
||||
the same clone after migration. Compare the result with the pre-migration audit, then test the
|
||||
candidate and predecessor against the migrated clone as required by the fleet runbook.
|
||||
|
||||
This runner does not prove that a database with a disposable-looking name is operationally
|
||||
disposable, does not prevent another application process from connecting, and does not acquire an
|
||||
activation-wide drain lock. The operator must create the clone from the intended fresh dump, keep
|
||||
it outside public service configuration, and destroy it separately after the release decision. A
|
||||
successful result authorizes only the next audit/test gate; it does not authorize a symlink switch,
|
||||
wallet action, payout, or traffic restoration.
|
||||
+103
-176
@@ -20,7 +20,10 @@ This guide covers deploying Wownerogue to a production environment.
|
||||
- [ ] Set secure database credentials
|
||||
- [ ] Configure wallet-rpc with authentication
|
||||
- [ ] Set `ADMIN_API_KEY` for admin endpoints
|
||||
- [ ] Firewall: expose only 80/443
|
||||
- [ ] Run `npm run preflight` with the final environment
|
||||
- [ ] Keep `PAYOUTS_ENABLED=false` on real-money prestige instances
|
||||
- [ ] Complete a stagenet payment and payout before enabling a payout instance
|
||||
- [ ] Firewall: allow Node 3000/3001 only from the reverse proxy; keep wallet/daemon RPC loopback-only
|
||||
- [ ] Protect `/admin.html` with basic auth
|
||||
- [ ] Set up database backups (see [LOGS_AND_BACKUP.md](./LOGS_AND_BACKUP.md))
|
||||
- [ ] Configure log rotation
|
||||
@@ -30,71 +33,35 @@ This guide covers deploying Wownerogue to a production environment.
|
||||
|
||||
## Create Dedicated User
|
||||
|
||||
Create an isolated system user for security:
|
||||
Create an isolated, non-login service identity. Release directories and the `current` link remain
|
||||
root-owned; the service identity receives read/traverse access only and must never own its code:
|
||||
|
||||
```bash
|
||||
# Create system user with no login shell and dedicated home
|
||||
sudo useradd --system --shell /usr/sbin/nologin --home-dir /var/www/wownerogue --create-home wownerogue
|
||||
|
||||
# Create .ssh directory for deploy key
|
||||
sudo mkdir -p /var/www/wownerogue/.ssh
|
||||
sudo chmod 700 /var/www/wownerogue/.ssh
|
||||
sudo chown wownerogue:wownerogue /var/www/wownerogue/.ssh
|
||||
sudo useradd --system --shell /usr/sbin/nologin --home-dir /var/www/wownerogue --no-create-home wownerogue
|
||||
sudo install -d -m 0750 -o root -g wownerogue /var/www/wownerogue
|
||||
sudo install -d -m 0750 -o root -g wownerogue /var/www/wownerogue/releases
|
||||
sudo install -d -m 0750 -o root -g wownerogue /etc/wownerogue
|
||||
|
||||
# Create log directory
|
||||
sudo mkdir -p /var/log/wownerogue
|
||||
sudo chown wownerogue:wownerogue /var/log/wownerogue
|
||||
sudo install -d -m 0750 -o wownerogue -g wownerogue /var/log/wownerogue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup Deploy Key and Clone Repository
|
||||
## Stage reviewed artifacts, not a repository clone
|
||||
|
||||
Generate an SSH deploy key for the wownerogue user:
|
||||
Build the runtime artifact from a clean, reviewed commit as described in
|
||||
[RELEASE_ARTIFACT.md](./RELEASE_ARTIFACT.md). The production host needs no Git checkout, deploy key,
|
||||
or write-capable source identity. The reviewed fleet
|
||||
`playbooks/wowngeon-stage-candidate.yml` role transfers exactly one tarball to a newly selected
|
||||
root-owned mode-0700 candidate inbox and compares its remote SHA-256 with the independently
|
||||
recorded literal digest. Follow the fleet Wowngeon runbook; the role is a one-shot custody boundary,
|
||||
not deployment.
|
||||
|
||||
```bash
|
||||
# Generate deploy key (as root, since user has no shell)
|
||||
sudo ssh-keygen -t ed25519 -C "wownerogue-deploy" -f /var/www/wownerogue/.ssh/id_ed25519 -N ""
|
||||
|
||||
# Set correct ownership and permissions
|
||||
sudo chown wownerogue:wownerogue /var/www/wownerogue/.ssh/id_ed25519*
|
||||
sudo chmod 600 /var/www/wownerogue/.ssh/id_ed25519
|
||||
sudo chmod 644 /var/www/wownerogue/.ssh/id_ed25519.pub
|
||||
|
||||
# Display the public key
|
||||
sudo cat /var/www/wownerogue/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
Add the deploy key to GitHub:
|
||||
1. Go to your repository → **Settings** → **Deploy keys**
|
||||
2. Click **Add deploy key**
|
||||
3. Paste the public key from the command above
|
||||
4. Leave "Allow write access" unchecked (read-only is sufficient)
|
||||
5. Click **Add key**
|
||||
|
||||
Configure SSH for GitHub:
|
||||
|
||||
```bash
|
||||
# Create SSH config for the wownerogue user
|
||||
sudo tee /var/www/wownerogue/.ssh/config << 'EOF'
|
||||
Host github.com
|
||||
HostName github.com
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking accept-new
|
||||
EOF
|
||||
|
||||
sudo chown wownerogue:wownerogue /var/www/wownerogue/.ssh/config
|
||||
sudo chmod 600 /var/www/wownerogue/.ssh/config
|
||||
```
|
||||
|
||||
Clone the repository:
|
||||
|
||||
```bash
|
||||
# Clone as the wownerogue user (replace YOUR_USERNAME with your GitHub username)
|
||||
sudo -u wownerogue git clone git@github.com:YOUR_USERNAME/wownerogue.git /var/www/wownerogue/app
|
||||
```
|
||||
The current fleet repository deliberately has no executable extraction/activation helper yet.
|
||||
Until that helper exists, the safe host boundary is blob-only staging: do not manually
|
||||
extract beneath `releases/`, install dependencies there, change `current`, restart an application,
|
||||
or run migrations merely because the blob has arrived.
|
||||
|
||||
---
|
||||
|
||||
@@ -112,77 +79,62 @@ node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
Install dependencies and audit for vulnerabilities:
|
||||
|
||||
```bash
|
||||
cd /var/www/wownerogue/app/src
|
||||
sudo -u wownerogue npm install
|
||||
|
||||
# Check for known vulnerabilities
|
||||
sudo -u wownerogue npm audit
|
||||
|
||||
# Auto-fix vulnerabilities where possible
|
||||
sudo -u wownerogue npm audit fix
|
||||
```
|
||||
The eventual reviewed extraction/activation helper installs exactly the locked production graph in its private,
|
||||
writable staging directory with `npm ci --omit=dev --ignore-scripts --no-audit --no-fund`, verifies
|
||||
it with `npm ls --omit=dev`, and seals the finished release root-owned and non-writable before it can
|
||||
be selected. Never make a release tree writable by `wownerogue`. Online advisory lookup is a
|
||||
separate explicitly authorized source-review operation; never run `npm audit fix` in staging or on
|
||||
a live release.
|
||||
|
||||
---
|
||||
|
||||
## Set File Permissions
|
||||
|
||||
```bash
|
||||
# Set ownership of application files
|
||||
sudo chown -R wownerogue:wownerogue /var/www/wownerogue
|
||||
|
||||
# Restrict permissions (owner only, no world access)
|
||||
sudo chmod 750 /var/www/wownerogue
|
||||
sudo chmod 640 /var/www/wownerogue/app/src/.env
|
||||
# Releases and selector are immutable to the service identity.
|
||||
sudo chown root:wownerogue /var/www/wownerogue /var/www/wownerogue/releases
|
||||
sudo chmod 0750 /var/www/wownerogue /var/www/wownerogue/releases
|
||||
sudo chmod 0640 /etc/wownerogue/app.env
|
||||
```
|
||||
|
||||
**Database permissions** - create a restricted PostgreSQL role:
|
||||
Do not use recursive ownership changes on `/var/www/wownerogue`; they can hand the service process
|
||||
write access to immutable code or the rollback selector. The reviewed extraction/activation helper must verify
|
||||
every individual release's ownership and modes before activation.
|
||||
|
||||
**Database ownership** - the application currently runs its own migrations at startup, so its
|
||||
database role must own the database/schema (or otherwise retain DDL rights). Create it this way:
|
||||
|
||||
```sql
|
||||
-- Create role with minimal privileges
|
||||
CREATE USER wownerogue WITH PASSWORD 'secure-password-here';
|
||||
CREATE DATABASE wownerogue OWNER wownerogue;
|
||||
|
||||
-- Revoke dangerous permissions
|
||||
REVOKE CREATE ON SCHEMA public FROM wownerogue;
|
||||
|
||||
-- Grant only what's needed
|
||||
GRANT CONNECT ON DATABASE wownerogue TO wownerogue;
|
||||
GRANT USAGE ON SCHEMA public TO wownerogue;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wownerogue;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO wownerogue;
|
||||
```
|
||||
|
||||
**Filesystem isolation** (optional, for extra hardening):
|
||||
|
||||
```bash
|
||||
# Prevent the user from accessing other users' home directories
|
||||
sudo chmod 700 /home/*
|
||||
|
||||
# Mount application directory with noexec for uploads (if applicable)
|
||||
# Add to /etc/fstab if using separate partition
|
||||
```
|
||||
|
||||
---
|
||||
Do not revoke schema `CREATE` from this role while application startup owns migration execution;
|
||||
that makes a fresh release fail partway through startup. A separate one-shot migrator role is the
|
||||
follow-up path if runtime DDL privileges need to be removed.
|
||||
|
||||
## Database Setup
|
||||
|
||||
### Run Migrations
|
||||
|
||||
Database migrations run automatically on server start (via `databaseManager.runMigrations()`), but you can also run them manually:
|
||||
Database migrations run automatically, in filename order, inside one transaction per file and are
|
||||
recorded in `schema_migrations`. Do not apply migration files directly with `psql`: bypassing the
|
||||
ledger makes startup attempt them again. Before a release, take a restricted PostgreSQL backup and
|
||||
test the complete migration set against a restored copy of each production database using the
|
||||
fail-closed [disposable clone migration gate](CLONE_MIGRATIONS.md).
|
||||
|
||||
```bash
|
||||
cd /var/www/wownerogue/app/src
|
||||
The migration ledger does not prove that rows predating a `NOT VALID` constraint are clean. Run
|
||||
the read-only historical audit and the rollback-only native validation proof described in
|
||||
[`FINANCIAL_CONSTRAINT_VALIDATION.md`](FINANCIAL_CONSTRAINT_VALIDATION.md) for each restored
|
||||
database. The native `VALIDATE CONSTRAINT` gate deliberately refuses live database names.
|
||||
|
||||
# Source environment variables
|
||||
source .env
|
||||
|
||||
# Run migrations manually if needed
|
||||
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f migrations/001_initial_schema.sql
|
||||
# ... repeat for each migration file in order
|
||||
```
|
||||
Install the reviewed `wowngeon-db-backup.sh`, `.service`, and `.timer` through the hash-pinned fleet
|
||||
operations change—not from the runtime artifact—and create
|
||||
`/var/backups/wowngeon/daily` as `postgres:postgres` mode `0700`, then enable the timer. Each run
|
||||
writes custom-format dumps atomically, verifies their catalogs with `pg_restore --list`, and records
|
||||
a SHA-256 sidecar. Configure retention/remote replication according to the operator's recovery
|
||||
policy; the supplied job deliberately does not delete backups.
|
||||
|
||||
### Database Reset (Development Only)
|
||||
|
||||
@@ -195,41 +147,21 @@ npm run db:create # Recreate schema
|
||||
|
||||
## systemd Service
|
||||
|
||||
Save to `/etc/systemd/system/wownerogue.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Wownerogue Game Server
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=wownerogue
|
||||
Group=wownerogue
|
||||
# Adjust paths if you cloned to a different location
|
||||
WorkingDirectory=/var/www/wownerogue/app/src
|
||||
EnvironmentFile=/var/www/wownerogue/app/src/.env
|
||||
ExecStart=/usr/bin/node index.js
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
ReadWritePaths=/var/www/wownerogue /var/log/wownerogue
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
Install reviewed application/firewall templates through the hash-pinned fleet operations change;
|
||||
the runtime artifact intentionally has no service units. Wallet-RPC replacement remains subject to
|
||||
the explicit NO-GO gates in [DEPLOY_INSTANCES.md](DEPLOY_INSTANCES.md). Application units read secrets from
|
||||
`/etc/wownerogue/app.env` or `/etc/monerogue/app.env`, not from an immutable release directory.
|
||||
They run `npm run preflight` before every start and use `/var/www/<instance>/current/src` so rollback
|
||||
is an atomic symlink switch.
|
||||
|
||||
Enable and start:
|
||||
|
||||
```bash
|
||||
# Create .env from example first!
|
||||
sudo -u wownerogue cp /var/www/wownerogue/app/src/.env.example /var/www/wownerogue/app/src/.env
|
||||
sudo vim /var/www/wownerogue/app/src/.env # Edit with your settings
|
||||
# Install the correct profile outside the release and replace every placeholder.
|
||||
sudo install -d -m 0750 -o root -g wownerogue /etc/wownerogue
|
||||
# Use the separately reviewed source/fleet copy; environment examples are not in the runtime archive.
|
||||
sudo install -m 0640 -o root -g wownerogue <reviewed-source>/src/.env.mainnet.example /etc/wownerogue/app.env
|
||||
sudoedit /etc/wownerogue/app.env
|
||||
|
||||
# Enable and start service
|
||||
sudo systemctl daemon-reload
|
||||
@@ -243,7 +175,7 @@ The systemd hardening options:
|
||||
- `ProtectSystem=strict` - mounts filesystem read-only except allowed paths
|
||||
- `ProtectHome=yes` - hides /home, /root, /run/user
|
||||
- `PrivateTmp=yes` - isolates /tmp
|
||||
- `ReadWritePaths` - whitelists writable directories
|
||||
- `ReadWritePaths` - whitelists the wallet state directory in wallet-RPC units
|
||||
|
||||
---
|
||||
|
||||
@@ -258,6 +190,10 @@ If using Nginx Proxy Manager:
|
||||
- Forward Port: `3000`
|
||||
- Enable "Websockets Support" toggle
|
||||
|
||||
Record the proxy's fixed source address in `/etc/wowngeon/firewall.env` as
|
||||
`NPM_SOURCE_IPV4=<address>`. Direct access to 3000/3001 must be rejected; this also makes the
|
||||
single-hop `TRUST_PROXY_HOPS=1` setting safe.
|
||||
|
||||
2. **SSL Tab**
|
||||
- Request a new SSL certificate
|
||||
- Enable "Force SSL"
|
||||
@@ -281,13 +217,15 @@ location /socket.io/ {
|
||||
|
||||
## Updating the Deployment
|
||||
|
||||
```bash
|
||||
cd /var/www/wownerogue/app
|
||||
sudo -u wownerogue git pull
|
||||
cd src && sudo -u wownerogue npm install
|
||||
sudo -u wownerogue npm audit fix
|
||||
sudo systemctl restart wownerogue
|
||||
```
|
||||
Run the full suite from the clean source commit, then build and verify the runtime artifact using
|
||||
[RELEASE_ARTIFACT.md](./RELEASE_ARTIFACT.md). Today, use the fleet blob-staging playbook and stop at
|
||||
the independently hash-verified root-owned candidate blob because the reviewed extraction/activation
|
||||
helper does not yet exist. When that helper implements the documented contract, it—not an application service identity or a
|
||||
manual shell session—will build a new immutable `/var/www/<instance>/releases/<release-id>`, install
|
||||
the locked graph, run the clone/preflight gates, seal ownership, and atomically select `current`.
|
||||
Never run `git pull`, `npm install`, or `npm audit fix` inside the active release. Keep the previous
|
||||
release and database backup until the new version has passed public health, WebSocket,
|
||||
payment-intake, and (stagenet only) payout smoke tests.
|
||||
|
||||
---
|
||||
|
||||
@@ -299,10 +237,11 @@ To run multiple instances (e.g., Wownero on port 3000 and Monero stagenet on por
|
||||
|
||||
```
|
||||
/var/www/
|
||||
├── wownerogue/ # Wownero instance (port 3000)
|
||||
│ └── app/src/.env
|
||||
└── monerogue/ # Monero instance (port 3001)
|
||||
└── app/src/.env
|
||||
├── wownerogue/ # releases/<id> + current symlink (port 3000)
|
||||
└── monerogue/ # releases/<id> + current symlink (port 3001)
|
||||
/etc/
|
||||
├── wownerogue/app.env # root:wownerogue, 0640
|
||||
└── monerogue/app.env # root:monerogue, 0640
|
||||
```
|
||||
|
||||
### Configuration Differences
|
||||
@@ -312,31 +251,15 @@ To run multiple instances (e.g., Wownero on port 3000 and Monero stagenet on por
|
||||
| `PORT` | 3000 | 3001 |
|
||||
| `CRYPTO_TYPE` | WOW | XMR |
|
||||
| `MONERO_NETWORK` | mainnet | stagenet |
|
||||
| `DB_NAME` | wownerogue | monerogue_stagenet |
|
||||
| `PRIMARY_WALLET_ENDPOINT` | http://127.0.0.1:34570 | http://127.0.0.1:38070 |
|
||||
| `DB_NAME` | wownerogue | monerogue |
|
||||
| `PRIMARY_WALLET_ENDPOINT` | http://127.0.0.1:34570 | http://127.0.0.1:38083 |
|
||||
| `PRIMARY_RPC_ENDPOINT` | http://127.0.0.1:34568 | http://127.0.0.1:38081 |
|
||||
|
||||
### Separate systemd Service
|
||||
|
||||
Create `/etc/systemd/system/monerogue.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Monerogue Game Server (Stagenet)
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=monerogue
|
||||
WorkingDirectory=/var/www/monerogue/app/src
|
||||
EnvironmentFile=/var/www/monerogue/app/src/.env
|
||||
ExecStart=/usr/bin/node index.js
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
Install the hash-pinned fleet copy of `monerogue.service`; it is intentionally absent from the
|
||||
runtime artifact. Do not hand-maintain a second, weaker unit. The reviewed template contains the
|
||||
same preflight, graceful-stop, immutable-release path, and systemd sandbox as the mainnet service.
|
||||
|
||||
---
|
||||
|
||||
@@ -374,10 +297,13 @@ journalctl -u wownerogue -f # Follow logs
|
||||
### Health Endpoint
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
curl http://localhost:3000/health/live
|
||||
curl --fail http://localhost:3000/health/ready
|
||||
```
|
||||
|
||||
Returns server uptime, memory usage, wallet status, and balance.
|
||||
`/health/live` checks the process. `/health/ready` returns 503 until PostgreSQL, the chain daemon,
|
||||
and (for paid instances) wallet RPC are ready. Public probes do not expose wallet balances or RPC
|
||||
endpoints; those remain in the authenticated admin dashboard.
|
||||
|
||||
### Admin Dashboard
|
||||
|
||||
@@ -397,8 +323,9 @@ Access `/admin.html` with your `ADMIN_API_KEY` for:
|
||||
# Check logs for errors
|
||||
journalctl -u wownerogue -n 50
|
||||
|
||||
# Verify environment file
|
||||
sudo -u wownerogue cat /var/www/wownerogue/app/src/.env
|
||||
# Validate the protected environment without printing it
|
||||
cd /var/www/wownerogue/current/src
|
||||
sudo -u wownerogue bash -c 'set -a; . /etc/wownerogue/app.env; set +a; npm run preflight'
|
||||
|
||||
# Test database connection
|
||||
psql -h localhost -U wownerogue -d wownerogue -c "SELECT 1;"
|
||||
@@ -408,17 +335,17 @@ psql -h localhost -U wownerogue -d wownerogue -c "SELECT 1;"
|
||||
|
||||
```bash
|
||||
# Test wallet RPC directly
|
||||
curl -u user:password -X POST http://127.0.0.1:34570/json_rpc \
|
||||
curl --digest -u user:password -X POST http://127.0.0.1:34570/json_rpc \
|
||||
-d '{"jsonrpc":"2.0","id":"0","method":"get_balance"}' \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
If migrations fail, check the `migrations` table:
|
||||
If migrations fail, check the `schema_migrations` ledger:
|
||||
|
||||
```sql
|
||||
SELECT * FROM migrations ORDER BY id;
|
||||
SELECT filename, applied_at FROM schema_migrations ORDER BY filename;
|
||||
```
|
||||
|
||||
Migrations are tracked by name to prevent duplicate execution.
|
||||
|
||||
+102
-65
@@ -5,38 +5,40 @@ Two independent instances of the same secured codebase, behind Nginx Proxy Manag
|
||||
| Instance | Domain | Port | Network | Money | Payouts | Leaderboards |
|
||||
|----------|--------|------|---------|-------|---------|--------------|
|
||||
| **wownerogue** (existing) | play.wowne.ro | 3000 | Wownero **mainnet** | real WOW | **OFF** | Pleb + Hall of Champions |
|
||||
| **monerogue** (new) | monerogue.app | 3001 | Monero **stagenet** | fake XMR | **ON** | Pleb + Hall of Champions |
|
||||
| **monerogue** (new) | monerogue.app | 3001 | Monero **stagenet** | fake XMR | **ON (target; wallet-gated)** | Pleb + Hall of Champions |
|
||||
|
||||
Stagenet payouts are a technical test surface, not authorization to offer real-money wagering.
|
||||
`ALLOW_MAINNET_PAYOUTS=true` is only an engineering interlock; obtain jurisdiction-specific legal,
|
||||
age/access-control, responsible-play, and operational review before attaching redeemable value.
|
||||
|
||||
NPM already forwards both domains to these ports + handles TLS — no reverse-proxy work here.
|
||||
|
||||
> ⚠️ Nothing below has been executed. Run only after review. Step 0 is the urgent one
|
||||
> (the live mainnet instance still runs pre-fix code with the underpayment exploit + payouts).
|
||||
|
||||
Server facts (verified): root+sudo; `monerod --stagenet` RPC on `127.0.0.1:38081`;
|
||||
`monero-wallet-rpc` at `/usr/bin`; stagenet wallet `…/monero-…-v0.18.4.4/test(.keys)`;
|
||||
Postgres has `wownerogue`,`wowrace`; ports `3001` and `38083` free; code at
|
||||
`/var/www/wownerogue/app` on branch `main` (commit 82fdcc0).
|
||||
> ⚠️ This is an operator runbook, not a record of actions already executed. Deploy only a reviewed,
|
||||
> tagged release after the full test suite and both preflights pass.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Get the secured branch onto the server
|
||||
## Step 0 — Stage an immutable reviewed release
|
||||
|
||||
```bash
|
||||
# Local: push the remediation branch (or merge to main first)
|
||||
git push origin security/phase-0-critical-fixes
|
||||
Build and verify one runtime artifact using [RELEASE_ARTIFACT.md](./RELEASE_ARTIFACT.md), then
|
||||
transfer only that blob into a new root-owned mode-0700 candidate inbox. Compare the host's SHA-256
|
||||
with the independently recorded literal digest; the transferred sidecar is not an independent
|
||||
provenance source.
|
||||
|
||||
# Server (mainnet instance):
|
||||
sudo -u wownerogue git -C /var/www/wownerogue/app fetch origin
|
||||
sudo -u wownerogue git -C /var/www/wownerogue/app checkout security/phase-0-critical-fixes
|
||||
cd /var/www/wownerogue/app/src && sudo -u wownerogue npm ci
|
||||
# Migrations run automatically on boot; or apply manually with the app's runner.
|
||||
```
|
||||
There is currently no reviewed fleet extraction/activation helper. Blob-only staging is allowed;
|
||||
manual extraction under `/var/www/<instance>/releases`, dependency installation, clone migration,
|
||||
preflight, service restart, and `current` changes remain blocked until that helper implements the
|
||||
complete ownership/path/hash/rollback contract. Never make a release directory writable by either
|
||||
application service identity.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Reconfigure the MAINNET instance: free-to-play, NO payouts
|
||||
|
||||
Edit `/var/www/wownerogue/app/src/.env` — change these keys (keep everything else):
|
||||
Install the reviewed source profile `src/.env.mainnet.example` as `/etc/wownerogue/app.env` (mode
|
||||
0640, `root:wownerogue`), replace every placeholder, and review these money gates. Environment
|
||||
examples are intentionally not present in the runtime artifact; provision this file through the
|
||||
hash-pinned fleet/source change, never from the application release directory.
|
||||
|
||||
```ini
|
||||
FREE_PLAY_ENABLED=true # offer free play as a choice (Pleb board)
|
||||
@@ -45,11 +47,16 @@ DIRECT_PAYOUTS_ENABLED=false
|
||||
CREDITS_PAYOUTS_ENABLED=false
|
||||
DIRECT_REQUIRES_ADDRESS=false # no payout = no payout address needed
|
||||
CREDITS_REQUIRES_ADDRESS=false
|
||||
TRUST_PROXY=true # we are behind NPM; read real client IP from X-Forwarded-For
|
||||
TRUST_PROXY=true
|
||||
TRUST_PROXY_HOPS=1 # exactly one NPM hop
|
||||
```
|
||||
|
||||
Players still buy credits / pay entry (→ Hall of Champions prestige); free players → Pleb
|
||||
board. No payouts = not gambling.
|
||||
board. This profile sends no winnings, but legal classification still depends on the product,
|
||||
jurisdiction, and access controls; obtain counsel before treating that as a legal conclusion.
|
||||
|
||||
The following are post-activation verification commands, not authorization to restart the live
|
||||
service while the wallet/preflight and fleet activation gates remain open:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart wownerogue.service
|
||||
@@ -57,46 +64,60 @@ journalctl -u wownerogue.service -n 50 --no-pager # verify: "Payment system: E
|
||||
curl -s localhost:3000/api/game-modes | jq '{freePlayEnabled, PAID_SINGLE:.PAID_SINGLE.payoutMultiplier}'
|
||||
```
|
||||
|
||||
**MAINNET WALLET NO-GO:** this application release does not authorize changing, copying, stopping,
|
||||
or replacing the Wownero wallet, its key file, password, unit, or RPC authentication. Keep payouts
|
||||
off. A later wallet change requires independently recoverable keyfile/password and mnemonic custody,
|
||||
an off-host recovery proof, an isolated rehearsal, exact address/balance verification, and a
|
||||
separately reviewed maintenance/rollback plan. Root-level wallet templates are operations source,
|
||||
not files in the runtime artifact and not approval to install them.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Stand up the MONERO STAGENET wallet-rpc
|
||||
## Step 2 — MONERO STAGENET wallet boundary: NO-GO
|
||||
|
||||
Do **not** install a replacement unit, create wallet identities/directories, copy wallet or key
|
||||
files, capture a password, provision RPC credentials, stop/restart the wallet, or change the live
|
||||
application endpoint as part of this release. The current wallet, unit, and application dependency
|
||||
remain unchanged.
|
||||
|
||||
The only retained design is
|
||||
[`README-monero-stagenet-parallel-candidate.md`](../scripts/deploy/README-monero-stagenet-parallel-candidate.md).
|
||||
It currently approves only a disposable synthetic fixture and, after independent review, a fixed
|
||||
read-only inventory preflight. Even its manual candidate service is not approved for installation.
|
||||
Before any host mutation, its complete crash/failure-injection gates, stopped-copy restart proof,
|
||||
offline address match, keyfile/password restore, mnemonic/off-host custody, and isolated unfunded
|
||||
rehearsal must all pass against an immutable reviewed hash.
|
||||
|
||||
Until a separate wallet promotion is approved, a stagenet application candidate must use an
|
||||
isolated database with payments, payouts, and match money disabled. Paid intake and the payout
|
||||
canary remain blocked; do not bypass application preflight to make them start.
|
||||
|
||||
## Step 3 — Create the monerogue identity, immutable layout, and DB
|
||||
|
||||
```bash
|
||||
sudo cp scripts/deploy/monero-wallet-rpc.service /etc/systemd/system/
|
||||
sudoedit /etc/systemd/system/monero-wallet-rpc.service # set REPLACE_WITH_WALLET_PASSWORD
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now monero-wallet-rpc.service
|
||||
sudo systemctl status monero-wallet-rpc.service --no-pager
|
||||
# sanity: wallet answers + is the stagenet wallet that holds the coins
|
||||
curl -s http://127.0.0.1:38083/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_balance"}' -H 'Content-Type: application/json'
|
||||
sudo useradd --system --home /var/www/monerogue --no-create-home --shell /usr/sbin/nologin monerogue
|
||||
sudo install -d -m 0750 -o root -g monerogue /var/www/monerogue
|
||||
sudo install -d -m 0750 -o root -g monerogue /var/www/monerogue/releases
|
||||
sudo install -d -m 0750 -o root -g monerogue /etc/monerogue
|
||||
|
||||
sudo -u postgres createuser --pwprompt monerogue
|
||||
sudo -u postgres createdb --owner=monerogue --template=template0 monerogue
|
||||
```
|
||||
|
||||
> Needs the wallet password (not in the repo). Confirm `…/v0.18.4.4/test` is the stagenet
|
||||
> wallet with the coins, or point `--wallet-file` at the right one.
|
||||
|
||||
## Step 3 — Create the monerogue user, code checkout, and DB
|
||||
|
||||
```bash
|
||||
sudo useradd --system --home /var/www/monerogue --shell /usr/sbin/nologin monerogue
|
||||
sudo mkdir -p /var/www/monerogue && sudo chown monerogue:monerogue /var/www/monerogue
|
||||
sudo -u monerogue git clone git@github.com:jwinterm/wownerogue.git /var/www/monerogue/app
|
||||
sudo -u monerogue git -C /var/www/monerogue/app checkout security/phase-0-critical-fixes
|
||||
cd /var/www/monerogue/app/src && sudo -u monerogue npm ci
|
||||
|
||||
sudo -u postgres createdb monerogue
|
||||
sudo -u postgres psql -c "CREATE USER monerogue WITH PASSWORD 'CHANGE_ME';"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE monerogue TO monerogue;"
|
||||
```
|
||||
The service identity owns its database role, not its application files. Do not clone source onto
|
||||
the production host or recursively `chown` `/var/www/monerogue` to the service user.
|
||||
|
||||
## Step 4 — The monerogue `.env`
|
||||
|
||||
Write `/var/www/monerogue/app/src/.env` (owned by `monerogue`, `chmod 600`). Stagenet/fake
|
||||
money, so payouts are fine. Amounts are XMR atomic units (1 XMR = 1e12):
|
||||
This is the eventual paid stagenet profile, not permission to activate it while Step 2 is NO-GO.
|
||||
Write `/etc/monerogue/app.env` as `root:monerogue` mode `0640`. Amounts are XMR atomic units
|
||||
(1 XMR = 1e12):
|
||||
|
||||
```ini
|
||||
NODE_ENV=production
|
||||
PORT=3001
|
||||
TRUST_PROXY=true
|
||||
TRUST_PROXY_HOPS=1
|
||||
CRYPTO_TYPE=XMR
|
||||
MONERO_NETWORK=stagenet
|
||||
HOSTED_BY=https://monerogue.app
|
||||
@@ -127,19 +148,19 @@ CREDITS_PACKAGES=[{"id":"small","credits":10,"price":"90000000000","bonus":0},{"
|
||||
ALLOW_MIXED_MODE=true
|
||||
PREFER_CREDITS_FIRST=true
|
||||
|
||||
# --- payouts --- (bankroll: ~0.3797 sXMR in the "test" wallet)
|
||||
# --- payouts --- (verify the unlocked stagenet bankroll before every rollout)
|
||||
PAYOUTS_ENABLED=true
|
||||
PAYOUT_MIN_AMOUNT=1000000000 # 0.001 XMR
|
||||
PAYOUT_MAX_PER_GAME=50000000000 # 0.05 XMR safety cap (real max is 3x*0.01 = 0.03)
|
||||
PAYOUT_BATCH_INTERVAL=300
|
||||
PAYOUT_MAX_RETRIES=3
|
||||
LOW_BALANCE_THRESHOLD=50000000000 # alert/halt when unlocked balance drops below 0.05 XMR
|
||||
BALANCE_WARN=100000000000 # warn below 0.10 XMR unlocked
|
||||
BALANCE_CRITICAL=50000000000 # stop accepting new payout liability below 0.05 XMR
|
||||
# Tip: lower DIRECT_GAME_PRICE/CREDITS_PAYOUT_BASE to 0.001 XMR (1000000000) for many more
|
||||
# demo plays per the small bankroll; refill from a stagenet faucet as needed.
|
||||
|
||||
# --- wallet RPC (monero stagenet) ---
|
||||
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:38083
|
||||
# wallet-rpc started with --disable-rpc-login, so no WALLET_RPC_USER/PASSWORD
|
||||
|
||||
# --- daemon RPC (monerod stagenet) ---
|
||||
PRIMARY_RPC_ENDPOINT=http://127.0.0.1:38081
|
||||
@@ -160,33 +181,49 @@ ADMIN_API_KEY=GENERATE_WITH_openssl_rand_hex_32
|
||||
SMIRK_ENABLED=false
|
||||
```
|
||||
|
||||
The application unit separately requires `/etc/monerogue/wallet-rpc.env`, owned
|
||||
`root:monerogue` mode `0640`, containing only `WALLET_RPC_USER` and `WALLET_RPC_PASSWORD`. Provision
|
||||
it through the approved secrets workflow only after the wallet gate clears. Never place either
|
||||
password in a command argument, unit `ExecStart`, shell history, or this runbook.
|
||||
|
||||
Production startup proves that wallet RPC actually enforces Digest authentication: an intentional
|
||||
unauthenticated, read-only probe must receive a usable `401` Digest challenge before the
|
||||
authenticated identity probe is accepted. Merely setting application credentials is insufficient;
|
||||
a wallet RPC that still accepts the unauthenticated request makes preflight/readiness fail closed.
|
||||
|
||||
```bash
|
||||
sudo chmod 600 /var/www/monerogue/app/src/.env
|
||||
sudo chown monerogue:monerogue /var/www/monerogue/app/src/.env
|
||||
sudo chmod 0640 /etc/monerogue/app.env
|
||||
sudo chown root:monerogue /etc/monerogue/app.env
|
||||
cd /var/www/monerogue/releases/<reviewed-release-id>/src
|
||||
sudo -u monerogue bash -c 'set -a; . /etc/monerogue/app.env; . /etc/monerogue/wallet-rpc.env; set +a; npm run preflight'
|
||||
```
|
||||
|
||||
## Step 5 — Install + start the monerogue instance
|
||||
|
||||
```bash
|
||||
sudo cp scripts/deploy/monerogue.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now monerogue.service
|
||||
journalctl -u monerogue.service -n 60 --no-pager # expect: listening on :3001, payments ENABLED
|
||||
curl -s localhost:3001/health | jq '{network:.network, wallet:.wallet.status, mode:.gameMode}'
|
||||
```
|
||||
The runtime artifact contains no service units. Install or update `monerogue.service` only through
|
||||
the separately reviewed, hash-pinned `~/src/such-fleet` change after the wallet and application
|
||||
gates pass. Do not copy a unit from the application release or start this paid profile while Step 2
|
||||
is NO-GO.
|
||||
|
||||
Install the hash-pinned fleet copies of `wowngeon-firewall.sh` and
|
||||
`wowngeon-firewall.service`, then set the fixed proxy source as `NPM_SOURCE_IPV4=<address>` in
|
||||
`/etc/wowngeon/firewall.env`. Verify direct access to ports 3000/3001 is rejected while both
|
||||
domains, websocket upgrades, and local readiness probes still work.
|
||||
|
||||
## Step 6 — Verify end to end
|
||||
|
||||
- `monerogue.app` loads; `play.wowne.ro` still healthy.
|
||||
- Stagenet: buy credits / pay entry with a stagenet wallet → game starts → win → payout
|
||||
arrives (this is the real `wownero-wallet-rpc`/`monero-wallet-rpc` integration test that
|
||||
TODO.md has always wanted — now on fake money).
|
||||
- Both boards populate: free game → Pleb, paid game → Hall of Champions, on each instance.
|
||||
- After a separate wallet GO and isolated canary: buy credits / pay entry with a stagenet wallet →
|
||||
game starts → win → payout arrives. This check is blocked while Step 2 is NO-GO.
|
||||
- After the corresponding gates clear, both boards populate: free game → Pleb, paid game → Hall of
|
||||
Champions, on each instance.
|
||||
- Mainnet: confirm a winning paid game does NOT send a payout (no crypto out).
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
- `sudo systemctl stop monerogue monero-wallet-rpc` (stagenet instance is independent; stopping it can't affect mainnet).
|
||||
- Mainnet: `git checkout main && systemctl restart wownerogue` to revert the reconfig.
|
||||
- Stop only the affected instance, atomically repoint `/var/www/<instance>/current` to the previous
|
||||
immutable release, and restart it. Keep the pre-deploy database backup for an operator-reviewed
|
||||
restore if a data rollback is required.
|
||||
- Never overwrite either legacy dirty checkout; preserve it until the new release has passed its soak.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Historical financial constraint release gate
|
||||
|
||||
Several migrations add `CHECK ... NOT VALID`. PostgreSQL enforces those constraints for new and
|
||||
changed rows, but it does not scan rows that existed before the constraint was added. A migration
|
||||
even a current migration ledger therefore does not, by itself, prove that all historical financial rows obey
|
||||
the current constraints.
|
||||
|
||||
Two repository-owned gates close that evidence gap without changing a live database.
|
||||
|
||||
## 1. Read-only historical audit
|
||||
|
||||
`scripts/deploy/financial-constraint-audit.sql` checks the exact connected database name, verifies
|
||||
the complete 18-constraint catalog inventory, and reports a named count for every historical
|
||||
predicate. It uses one repeatable-read, read-only transaction and rolls it back. Connection and
|
||||
credential handling remain operator-specific; do not put a database password in command history.
|
||||
|
||||
```bash
|
||||
psql -X -v ON_ERROR_STOP=1 \
|
||||
--set=expected_database=EXACT_DATABASE_NAME \
|
||||
--file=scripts/deploy/financial-constraint-audit.sql
|
||||
```
|
||||
|
||||
Exit status `0` means all required constraints exist and every old row satisfies them. Any missing
|
||||
constraint or nonzero category exits `3`. Reconcile violations with a separately reviewed,
|
||||
backup-backed repair; this audit intentionally has no repair mode.
|
||||
|
||||
This check may be run read-only against production and should also run against every restored
|
||||
release candidate database. It reports how many constraints remain catalog-marked unvalidated;
|
||||
that count is informational because the second gate proves PostgreSQL's validator itself.
|
||||
|
||||
## 2. Explicit validation on a disposable restore
|
||||
|
||||
`scripts/deploy/financial-constraint-validate-restore.sql` includes the read-only audit, then issues
|
||||
an explicit `ALTER TABLE ... VALIDATE CONSTRAINT` for all 18 constraints inside a transaction. It
|
||||
checks `pg_constraint.convalidated` and rolls the entire transaction back, so no catalog flag is
|
||||
durably changed.
|
||||
|
||||
`VALIDATE CONSTRAINT` still scans tables, takes locks, and transiently updates PostgreSQL catalogs.
|
||||
For that reason this script fails before `BEGIN` unless all three conditions hold:
|
||||
|
||||
- the connected name exactly equals `expected_database`;
|
||||
- the database name contains `restore` or `scratch`;
|
||||
- the exact disposable-restore confirmation is supplied.
|
||||
|
||||
Run it only against a freshly restored, isolated database with no application traffic:
|
||||
|
||||
```bash
|
||||
psql -X -v ON_ERROR_STOP=1 \
|
||||
--set=expected_database=monerogue_release_restore \
|
||||
--set=confirm_disposable=I_CONFIRM_THIS_DATABASE_IS_A_DISPOSABLE_RESTORE \
|
||||
--file=scripts/deploy/financial-constraint-validate-restore.sql
|
||||
```
|
||||
|
||||
A pass proves both the explicit historical predicates and PostgreSQL's native validation path on
|
||||
that restored data. It does not mark live constraints validated. A later durable live
|
||||
`VALIDATE CONSTRAINT` operation requires its own reviewed maintenance plan, backup/restore proof,
|
||||
lock budget, and rollback decision; do not fold that mutation into application startup.
|
||||
|
||||
## Static coverage
|
||||
|
||||
The focused test inventories every migration occurrence of `NOT VALID` and fails if either gate
|
||||
omits a constraint:
|
||||
|
||||
```bash
|
||||
npm --prefix src test -- --runTestsByPath ../test/financialConstraintValidation.test.js
|
||||
```
|
||||
|
||||
Adding another `NOT VALID` constraint requires adding its exact historical predicate, its explicit
|
||||
restore validation statement, and its table/name entry to both gates.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Financial reconciliation
|
||||
|
||||
The admin dashboard has separate **Payouts** and **Refunds** views. Start with the
|
||||
**Needs Attention** filter and compare the row, application logs, and wallet history.
|
||||
The overview and each list show `processing` and `needs_review` counts; these are
|
||||
liabilities, not failed transfers that can safely be resent.
|
||||
|
||||
## State rules
|
||||
|
||||
- A payout in `processing` or `needs_review` may already have broadcast. Never use the
|
||||
retry action for it.
|
||||
- The payout retry action is available only for a hashless `failed` or
|
||||
`permanently_failed` row. Use it only after proving the recorded error happened before
|
||||
broadcast and confirming that wallet history contains no matching transfer. The UI
|
||||
sends the API's required explicit confirmation.
|
||||
- A refund in `requested` has not yet been claimed by the refund worker. A refund in
|
||||
`processing` may have broadcast. A refund in `needs_review` is quarantined. There is no
|
||||
dashboard resend action for any refund state.
|
||||
- `recorded` means entitlements/payment state was reversed without an on-chain transfer;
|
||||
`completed` requires transaction-hash evidence.
|
||||
|
||||
## Reconciliation checklist
|
||||
|
||||
1. Preserve the database row, service logs, wallet files, and current backups before
|
||||
changing anything.
|
||||
2. Match currency/network, destination, exact atomic amount, creation/processing time,
|
||||
transaction hash (if present), and wallet outgoing-transfer history.
|
||||
3. If a matching wallet transfer exists, record its hash and confirmation evidence in an
|
||||
audited remediation. Do not create a replacement transfer.
|
||||
4. If no transfer appears, treat that as inconclusive until the wallet is synchronized and
|
||||
the relevant time window and mempool are checked. Escalate ambiguous cases instead of
|
||||
retrying.
|
||||
5. Re-run the read-only financial audit after any reviewed database remediation and retain
|
||||
the before/after evidence with the incident record.
|
||||
|
||||
Periodic alerts use the same aggregate states. `FINANCIAL_REVIEW_STALE_MS` controls when a
|
||||
payout `processing` row or refund `requested`/`processing` row is considered stale; the
|
||||
default is 15 minutes. Alerts are read-only and never move or retry a transfer.
|
||||
|
||||
There is intentionally no generic "mark resolved" endpoint. A reviewed state transition
|
||||
needs a case-specific, audited remediation because the correct result depends on wallet
|
||||
evidence and whether value was actually transferred.
|
||||
+99
-39
@@ -21,21 +21,32 @@ starts.
|
||||
## Lifecycle: a race every block
|
||||
|
||||
The existing solo `queueHandler` already reacts to new crypto blocks. `MatchScheduler` subscribes to
|
||||
the same block event and drains per-economy queues:
|
||||
the same block-count event. Free and value-bearing queues intentionally have different starts:
|
||||
|
||||
- If a queue has ≥2 players at block H, all queued players are placed into a new `MatchRoom` and the
|
||||
race starts immediately.
|
||||
- If a queue has 1 player, they carry over to block H+1 and may leave at any time.
|
||||
- A free queue with ≥2 players drains into a `MatchRoom` immediately on the current block event.
|
||||
- A paid queue with ≥2 players at canonical header H atomically creates a `pending` match and links
|
||||
the exact FIFO queue rows and entrants. That transaction records a deterministic entrant-freeze
|
||||
commitment and fixes canonical header H+the configured entropy delay as its target; it does not
|
||||
read a block hash or create a playable seed. A fresh post-commit count is persisted while the
|
||||
target is still in the future.
|
||||
- Only a later, sufficiently confirmed block-count event can resolve that pending match. The
|
||||
scheduler twice requests the canonical hash for the exact target, verifies the frozen IDs again,
|
||||
derives the seed, and changes the match from `pending` to `starting` before collection/countdown.
|
||||
It immediately checks the hash again. Another height and server randomness are not substitutes.
|
||||
- If a queue has 1 player, they carry over and may leave until a freeze claims their row.
|
||||
- If a queue is empty, nothing happens.
|
||||
|
||||
A race ends when:
|
||||
1. The first player reaches the exit.
|
||||
2. The next block is detected (with a `MATCH_MIN_DURATION_MS` floor to prevent degenerate short
|
||||
races).
|
||||
A match ends when:
|
||||
1. Its selected ruleset resolves (for example, first exit, last alive, or all players resolved).
|
||||
2. If `ruleset.timing.blockDeadline` is enabled, the first advancing canonical header after both
|
||||
the match's start header and `MATCH_MIN_DURATION_MS` of active play is observed. A duplicate
|
||||
block poll, the start header itself, or a block arriving before the floor cannot end the match;
|
||||
after an early block, the match waits for the next advancing header.
|
||||
3. The `MATCH_HARD_CEILING_MS` absolute ceiling expires.
|
||||
|
||||
If no one escaped, living players are ranked by proximity to the exit; dead players are ranked after
|
||||
them by progress, treasure, and moves.
|
||||
Final placement and `winnerId` come from the selected ruleset's deterministic rank strategy. The
|
||||
classic race uses exit/proximity ranking; last-alive and score-attack use their own competitive
|
||||
rankings.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -63,7 +74,8 @@ them by progress, treasure, and moves.
|
||||
|
||||
See migration `src/migrations/022_match_mode.sql`:
|
||||
|
||||
- `matches` — one race, provably-fair seed, economy, pot/fee, winner.
|
||||
- `matches` — one race, including the durable `pending` entrant-freeze envelope and later
|
||||
verifiable seed commitment, economy, pot/fee, winner.
|
||||
- `match_entrants` — per-player state, placement, score, payment link.
|
||||
- `match_events` — replay / spectator / audit feed.
|
||||
- `match_queue_entries` — persisted queue for restart safety.
|
||||
@@ -71,11 +83,35 @@ See migration `src/migrations/022_match_mode.sql`:
|
||||
- `users.race_entries` — hot-read ticket balance.
|
||||
- `payouts.match_id` — links winner payout to match.
|
||||
|
||||
### Provably fair
|
||||
### Match seed verification
|
||||
|
||||
Each match commits to a SHA-256 `seed_hash` at creation. The `seed` is revealed on finish so anyone
|
||||
can regenerate the exact dungeon and verify the hash. This mirrors the existing solo
|
||||
`games.dungeon_seed` / `games.seed` pattern.
|
||||
Paid matches use `future-block-freeze-v2` / `future-chain-block-v2`. At canonical header H the
|
||||
server commits the exact economy, ruleset, sorted durable FIFO queue-entry IDs, configured delay,
|
||||
and target `H + MATCH_PAID_ENTROPY_DELAY_BLOCKS` in a `pending` row before requesting any hash.
|
||||
After that transaction commits, a fresh strict daemon count must prove that the target does not yet
|
||||
exist; the witnessed tip and verification timestamp are durable and immutable. A delayed commit
|
||||
that reaches the target is cancelled and every exact escrow anchor is refunded without reading the
|
||||
target hash. Legacy v1 or otherwise unverified pending freezes are also refunded at startup.
|
||||
|
||||
Activation waits until the target itself has the configured confirmation depth (the target counts
|
||||
as confirmation one). The scheduler reads count/header/count/header and requires both exact-height
|
||||
headers to have the same canonical hash. It re-reads that target immediately after the activation
|
||||
transaction and before ticket collection, gameplay, or client notification; an unavailable or
|
||||
changed hash aborts and refunds the match. The playable seed is SHA-256-derived only from the v2
|
||||
freeze commitment and exact target hash. The persisted/public proof records the delay, required
|
||||
confirmations, minimum activation tip, and post-commit witness. Confirmation settings are safety
|
||||
metadata rather than extra seed material.
|
||||
|
||||
Monero-family `getblockcount` is a count, while `get_block(height)` uses zero-based header heights.
|
||||
The scheduler normalizes the event exactly once (`observedHeaderHeight = blockCount - 1`). Every
|
||||
persisted/disclosed `blockHeight` is the actual header height passed unchanged to `get_block`, so the
|
||||
published height resolves to the published hash without an off-by-one translation.
|
||||
|
||||
This proves the documented seed derivation and deterministic dungeon; it does not prove honest
|
||||
input handling, block-source independence, payout delivery, or resistance to a malicious chain
|
||||
producer/operator. Free development matches may use a server-random commitment and are labelled
|
||||
`server commitment only`. Do not describe match mode as equivalent to the solo two-party fairness
|
||||
protocol without an independent review.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -85,12 +121,21 @@ Add to `src/.env` (documented in `src/.env.example`):
|
||||
# Enable match mode (default false)
|
||||
MATCH_ENABLED=false
|
||||
|
||||
# Max players per race (2–32)
|
||||
# Max players per match (also bounded by the selected ruleset; built-ins max at 8)
|
||||
MATCH_MAX_PLAYERS=4
|
||||
|
||||
# Server tick interval in ms
|
||||
MATCH_TICK_MS=250
|
||||
|
||||
# Server-selected gameplay ruleset; clients cannot override it
|
||||
# race | last-alive | score-attack | coop-escape
|
||||
MATCH_RULESET_ID=race
|
||||
|
||||
# Paid freeze target distance and activation confirmation depth.
|
||||
# Production requires explicit safe integers from 2 through 100.
|
||||
MATCH_PAID_ENTROPY_DELAY_BLOCKS=2
|
||||
MATCH_PAID_ENTROPY_CONFIRMATIONS=2
|
||||
|
||||
# Minimum race duration before the next block can end it
|
||||
MATCH_MIN_DURATION_MS=20000
|
||||
|
||||
@@ -100,20 +145,25 @@ MATCH_HARD_CEILING_MS=240000
|
||||
# Credits cost for prestige-only races
|
||||
MATCH_CREDITS_COST=1
|
||||
|
||||
# Crypto race house fee percent (default 5)
|
||||
# Crypto race house fee percent (required; 0 <= fee < 100)
|
||||
MATCH_HOUSE_FEE_PERCENT=5
|
||||
|
||||
# Optional: override per-player entry fee atomic amount.
|
||||
# If unset, uses the existing per-game price (singleGamePrice / DIRECT_GAME_PRICE).
|
||||
# MATCH_ENTRY_FEE_ATOMIC=5000000000
|
||||
# Required per-player funded ticket value in atomic units.
|
||||
MATCH_ENTRY_FEE_ATOMIC=5000000000
|
||||
|
||||
# Enable the crypto race-entry ticket economy
|
||||
# Crypto admission requires both explicit match gates and an outer atomic-unit payout cap
|
||||
MATCH_CRYPTO_RACE_ENABLED=false
|
||||
MATCH_PAYOUTS_ENABLED=false
|
||||
MATCH_PAYOUT_MAX=50000000000
|
||||
```
|
||||
|
||||
Crypto races also require:
|
||||
- A paid instance (`GAME_MODE=PAID_SINGLE` / `PAID_CREDITS` or equivalent `PAYMENT_MODES`).
|
||||
- Race-entry ticket products in the credit/cosmetic catalog with `grants.race_entries: N`.
|
||||
- At least one race-entry product with `grants.race_entries: N` and
|
||||
`grants.race_entry_value_atomic` exactly equal to `MATCH_ENTRY_FEE_ATOMIC`. The product price
|
||||
must cover `N * MATCH_ENTRY_FEE_ATOMIC`; confirmation records a durable lot per payment.
|
||||
- A competitive single-winner ruleset. `coop-escape` is rejected until split-payout semantics are
|
||||
implemented.
|
||||
|
||||
## Client surfaces
|
||||
|
||||
@@ -125,7 +175,7 @@ Crypto races also require:
|
||||
## Socket events
|
||||
|
||||
Server emits:
|
||||
- `game_mode_info` — now includes `modes.match { enabled, economies, maxPlayers }`.
|
||||
- `game_mode_info` — includes `modes.match { enabled, economies, maxPlayers, activeRuleset, rulesets }`.
|
||||
- `match_queue_joined` / `match_queue_left` — queue responses.
|
||||
- `match_joined` / `match_start` / `match_tick` / `match_end` — race lifecycle for players.
|
||||
- `tavern_match_list` / `tavern_match_tick` / `tavern_match_end` — public race feed for tavern
|
||||
@@ -139,11 +189,17 @@ Client sends:
|
||||
|
||||
## Leaderboards
|
||||
|
||||
- **Free races** create synthetic `games` rows with `game_mode = 'FREE'` for the Pleb board.
|
||||
- **Credits/prestige races** populate the `prestige_leaderboard` view and are served by
|
||||
`GET /api/leaderboard?board=prestige`.
|
||||
- **Crypto races** create synthetic `games` rows with `game_mode = 'PAID_CREDITS'` for the Hall of
|
||||
Champions board.
|
||||
- **Free competitive matches** create synthetic `games` rows with `game_mode = 'FREE'` for the
|
||||
Pleb board.
|
||||
- **Credits/prestige competitive matches** are served from authoritative `matches` and
|
||||
`match_entrants` rows by `GET /api/leaderboard?board=prestige`.
|
||||
- **Crypto competitive matches** create synthetic `games` rows with `game_mode = 'PAID_CREDITS'`
|
||||
for the Hall of Champions board.
|
||||
|
||||
An individual win requires the durable match winner and placement #1 to agree; reaching the exit
|
||||
alone is never treated as a win in last-alive or score-attack. `coop-escape` is collective, records
|
||||
no individual `winnerId`, and keeps its results in `matches` / `match_entrants`; it is intentionally
|
||||
excluded from individual boards until a dedicated team leaderboard exists.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -154,15 +210,8 @@ cd src
|
||||
npx jest ../test/match
|
||||
```
|
||||
|
||||
Current result: **40 passing** across 7 suites:
|
||||
|
||||
- `matchRoom.test.js`
|
||||
- `matchQueue.test.js`
|
||||
- `matchScheduler.test.js`
|
||||
- `matchLeaderboard.test.js`
|
||||
- `matchPayoutService.test.js`
|
||||
- `tavernMatchBridge.test.js`
|
||||
- `matchReconnect.test.js`
|
||||
The suite includes block-deadline, authoritative leaderboard-result, co-op exclusion, durable
|
||||
finish, and finished-room reconnect regressions in addition to the core room/queue/payout tests.
|
||||
|
||||
## Files added / modified
|
||||
|
||||
@@ -188,7 +237,18 @@ Modified:
|
||||
## Operational notes
|
||||
|
||||
- Match mode is **opt-in** and inert unless `MATCH_ENABLED=true`.
|
||||
- `MATCH_RULESET_ID` is trusted server configuration; unknown/solo-only values fall back to `race`.
|
||||
- Crypto race payouts reuse the existing `payouts` table, batch processor, and retry service.
|
||||
- The unique partial index `idx_payouts_one_per_match` prevents double payouts.
|
||||
- Race-entry tickets are held in escrow on queue join and consumed only when a match starts.
|
||||
- `MATCH_PAYOUTS_ENABLED=false` stops new admission and refunds queued ticket escrow. Matches whose
|
||||
pot/liability was already accepted still create their durable payout; the global payout master
|
||||
switch may pause dispatch without erasing it.
|
||||
- The unconditional unique index `idx_payouts_one_per_match` prevents a replacement payout in every
|
||||
status, including `failed` and `needs_review`.
|
||||
- Only confirmed-payment ticket lots with the current exact entry value can enter a crypto race;
|
||||
legacy/admin-granted aggregate tickets cannot create a payout liability. The exact lot is held
|
||||
on queue join and consumed in the same transaction that accepts the immutable match liability.
|
||||
- Players who disconnect during a race have a 30-second grace period to reconnect.
|
||||
- Paid freeze/start block events are serialized. A crash before activation leaves a resumable
|
||||
`pending` freeze; a crash after activation is handled by abandoned-match recovery. Orderly
|
||||
shutdown stops the scheduler, waits for its block task, and transactionally cancels/refunds every
|
||||
still-pending freeze before shutting down match transport.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Production disclosures and paid-action gate
|
||||
|
||||
This implementation makes the running server describe its actual free, paid-prestige, payout,
|
||||
test-network, and PvP switches. It does **not** claim that the product is licensed or lawful in a
|
||||
particular place. Obtain jurisdiction-specific advice before accepting assets with real value.
|
||||
|
||||
## Required production settings
|
||||
|
||||
Every production instance with `PAYMENTS_ENABLED=true` fails preflight/startup unless all of these
|
||||
are explicit and valid:
|
||||
|
||||
```dotenv
|
||||
OPERATOR_NAME=Truthful responsible operator name
|
||||
OPERATOR_CONTACT_URL=mailto:support@example.com
|
||||
OPERATOR_CONTACT_LABEL=Support
|
||||
LEGAL_POLICY_VERSION=2026-07-21-v1
|
||||
TERMS_EFFECTIVE_DATE=2026-07-21
|
||||
MINIMUM_AGE=18
|
||||
PAID_ACKNOWLEDGEMENT_REQUIRED=true
|
||||
```
|
||||
|
||||
Production match mode also requires explicit paid-fairness timing, even if only prestige credits
|
||||
are presently exposed:
|
||||
|
||||
```dotenv
|
||||
MATCH_PAID_ENTROPY_DELAY_BLOCKS=2
|
||||
MATCH_PAID_ENTROPY_CONFIRMATIONS=2
|
||||
```
|
||||
|
||||
Both are bounded safe integers from 2 through 100. Paid PvP persists the actual delay,
|
||||
confirmation requirement, activation threshold, and post-commit daemon-tip witness in its public
|
||||
proof; changing these settings does not reinterpret an already-frozen match.
|
||||
|
||||
`RESTRICTED_LOCATIONS_NOTICE` is optional application copy. Add it only after legal review; text is
|
||||
not geofencing. Changing the terms or material disclosures requires a new `LEGAL_POLICY_VERSION`,
|
||||
which invalidates prior browser-session acknowledgements.
|
||||
|
||||
## Enforcement contract
|
||||
|
||||
- `/api/disclosures` returns non-secret, no-store policy data derived from runtime switches.
|
||||
- `/terms`, `/privacy`, and `/responsible-play` render the active mode and operator metadata.
|
||||
- Before a payment invoice, paid solo/credit entry, or paid PvP queue join, the browser requires
|
||||
unselected age/reading/risk boxes. Test networks require an additional mainnet/test-coin box.
|
||||
- Acceptance is a five-field canonical record (policy version plus four booleans), scoped to the
|
||||
current browser tab and policy version, and cleared on rejection or socket disconnect.
|
||||
- The server validates the exact current version and booleans before consuming a fairness offer,
|
||||
opening an invoice, consuming a credit, or escrowing a paid PvP entry. A modified client cannot
|
||||
bypass this check.
|
||||
- Free entry remains available without a paid acknowledgement where the instance offers it.
|
||||
|
||||
The acknowledgement is a player statement, not proof of identity, age, capacity, or location.
|
||||
|
||||
## Mode language
|
||||
|
||||
| Runtime mode | Player-facing contract |
|
||||
|---|---|
|
||||
| Free | No entry payment; free leaderboard; no free payout |
|
||||
| Paid, payouts off | Entry/credits/product purchase and paid prestige leaderboard; no crypto prize |
|
||||
| Paid, solo payouts on | Full entry can be lost; only pre-entry recorded outcomes/amounts qualify |
|
||||
| Crypto PvP on | Paid ticket escrow and configured winner/fee contract; separate from free/prestige PvP |
|
||||
| XMR stagenet/testnet | Valueless test currency; explicit warning never to send mainnet XMR |
|
||||
|
||||
## Unresolved launch decisions
|
||||
|
||||
Technical copy and checkboxes do not resolve these. Keep real-value modes gated until the responsible
|
||||
operator records decisions for:
|
||||
|
||||
1. entity/operator identity, licences/registrations, permitted and excluded locations;
|
||||
2. age and location verification appropriate to those locations;
|
||||
3. consumer terms: refunds, late/under/overpayments, complaints, dispute venue, and downtime;
|
||||
4. privacy roles, processors, retention/deletion schedule, user-rights procedure, breach response,
|
||||
and whether a privacy-minimized durable acknowledgement record is legally required;
|
||||
5. responsible-play features such as account-level deposit/time/loss limits and durable self-exclusion;
|
||||
6. tax, sanctions/AML, accounting, and cryptocurrency reporting obligations;
|
||||
7. an independent review of game rules, advertised multipliers, house fee, bankroll, and provable-fair claims.
|
||||
|
||||
Stagenet can be used for end-to-end mechanics without representing test coins as money. Mainnet
|
||||
payouts remain a separate, explicit operational and legal decision.
|
||||
|
||||
## Front-end dependency note
|
||||
|
||||
The same-origin jQuery copy is 3.7.1 from the Ubuntu `libjs-jquery` package; exact provenance and
|
||||
SHA-256 are in `html/js/lib/jquery-3.7.1.PROVENANCE.md`. The CSP still permits inline script/style
|
||||
because current pages depend on them. A nonce migration must move every inline handler/script before
|
||||
removing `unsafe-inline`; do not partially tighten it and strand payment/game controls.
|
||||
@@ -0,0 +1,150 @@
|
||||
# Deterministic PvP capture harness
|
||||
|
||||
`src/scripts/pvp-capture.js` creates a clean, reproducible multiplayer match for trailers, ads,
|
||||
camera work, and gameplay review. It supports the built-in Escape Race, Last Alive, Score Attack,
|
||||
and Co-op Escape rulesets. Each take uses the authoritative `MatchRoom` and `MatchEngine`, drives
|
||||
deterministic bots through the installed Socket.IO client, and records the existing browser render
|
||||
kit with the installed Playwright Chromium.
|
||||
|
||||
This is intentionally not a production feature or an admin endpoint. The harness:
|
||||
|
||||
- starts a separate HTTP/Socket.IO process bound to `127.0.0.1` on an ephemeral port;
|
||||
- accepts no remote target URL and cannot attach to a normal Wowngeon instance;
|
||||
- creates one in-memory `free` match with zero entry fee, pot, house fee, or payout;
|
||||
- imports no application, database, wallet, queue, provider, or payout service;
|
||||
- requires `NODE_ENV=development` or `NODE_ENV=test` and the explicit
|
||||
`--confirm-local-free-only` flag;
|
||||
- refuses to run when a payment, credits, payout, crypto-race, mainnet-payout, or wallet endpoint
|
||||
is enabled/configured; and
|
||||
- refuses inherited crypto, game-mode, difficulty, dungeon, monster, treasure, or network-tuning
|
||||
overrides so a committed seed cannot silently mean different footage in another shell; and
|
||||
- loads only repository assets. CDN/3D renderers are unavailable in this tool.
|
||||
|
||||
Those guards make setting an environment variable in the live service insufficient to enable the
|
||||
harness. Someone must deliberately start this separate loopback-only CLI with its confirmation
|
||||
flag. On a staging machine, run it as a separate development/test process; it never uses the
|
||||
staging app or staging database.
|
||||
|
||||
## Record a video
|
||||
|
||||
From `src/`:
|
||||
|
||||
```bash
|
||||
NODE_ENV=development npm run capture:pvp -- \
|
||||
--confirm-local-free-only \
|
||||
--ruleset race \
|
||||
--players 4 \
|
||||
--mode tiles \
|
||||
--output /tmp/wowngeon-pvp-capture.webm \
|
||||
--screenshot /tmp/wowngeon-pvp-final.png
|
||||
```
|
||||
|
||||
The repository already declares `playwright-core`; the command uses its existing local Chromium
|
||||
and FFmpeg bundle. It never downloads a browser or package. If that browser bundle is missing, the
|
||||
CLI fails with a direct explanation. The deterministic control harness remains usable without a
|
||||
browser:
|
||||
|
||||
```bash
|
||||
NODE_ENV=test npm run capture:pvp -- \
|
||||
--confirm-local-free-only \
|
||||
--no-video \
|
||||
--ticks 120 \
|
||||
--trace /tmp/wowngeon-pvp-trace.json
|
||||
```
|
||||
|
||||
Run `npm run capture:pvp -- --help` for every option. `--ruleset` accepts `race`, `last-alive`,
|
||||
`score-attack`, or `coop-escape`; this is separate from `--mode`, whose offline renderer choices
|
||||
are `tiles`, `ascii`, and `iso`. Every ruleset accepts 2–8 bots in this harness.
|
||||
|
||||
The default 1280×720 spectator camera anchors on `bot-1`; choose another player with `--focus
|
||||
bot-N`. In the default `--camera action` mode, deaths, treasure pickups, and exits temporarily take
|
||||
focus before the camera returns to that anchor. `--camera focus` disables those cuts. Camera zoom
|
||||
adjusts to the active pack: it pushes in for close action and pulls back as players spread out.
|
||||
Portrait viewports begin with a tighter follow crop so 9:16 footage fills the frame. A separate
|
||||
screen-space roster keeps all 2–8 player names and states legible without stacking labels on one
|
||||
dungeon tile.
|
||||
|
||||
## Capture each built-in mode
|
||||
|
||||
Use the same safety acknowledgement for every take:
|
||||
|
||||
| `--ruleset` | Bot objective | Result card |
|
||||
|---|---|---|
|
||||
| `race` | Treasure route plus staggered sprints to the exit | Competitive race winner |
|
||||
| `last-alive` | Seek and strike the nearest active rival | Last survivor |
|
||||
| `score-attack` | Build progress/treasure score, then bank an exit | Top score and points |
|
||||
| `coop-escape` | Staggered team pathing to the exit | Team escape count, never a fake competitive winner |
|
||||
|
||||
For example, record a Last Alive take:
|
||||
|
||||
```bash
|
||||
NODE_ENV=development npm run capture:pvp -- \
|
||||
--confirm-local-free-only \
|
||||
--ruleset last-alive \
|
||||
--players 6 \
|
||||
--camera action \
|
||||
--viewport 1920x1080 \
|
||||
--output /tmp/wowngeon-last-alive-ad.webm \
|
||||
--trace /tmp/wowngeon-last-alive-ad.json
|
||||
```
|
||||
|
||||
Swap only `--ruleset` and output names to capture Score Attack or Co-op Escape. Headlines, active
|
||||
player terminology, roster states, footer copy, and final results all follow the selected ruleset.
|
||||
Score Attack closes the local exhibition deadline as soon as every bot has died or banked an exit,
|
||||
avoiding a motionless tail without changing the authoritative scoring/finalization path.
|
||||
|
||||
For a short 9:16 ad take, the curated seed produces a complete six-racer arc in about nine seconds.
|
||||
Follow the eventual winner so the finish remains legible on a phone screen:
|
||||
|
||||
```bash
|
||||
NODE_ENV=development npm run capture:pvp -- \
|
||||
--confirm-local-free-only \
|
||||
--ruleset race \
|
||||
--players 6 \
|
||||
--focus bot-4 \
|
||||
--viewport 1080x1920 \
|
||||
--output /tmp/wowngeon-pvp-ad-vertical.webm \
|
||||
--screenshot /tmp/wowngeon-pvp-ad-vertical.png
|
||||
```
|
||||
|
||||
Use the emitted JSON trace as the authoritative replay record. The video is marketing footage; it
|
||||
does not create a leaderboard row, credit, payment, ticket, or payout.
|
||||
|
||||
For a crowded 16:9 arena take, the same seed supports the built-in eight-player maximum:
|
||||
|
||||
```bash
|
||||
NODE_ENV=development npm run capture:pvp -- \
|
||||
--confirm-local-free-only \
|
||||
--ruleset race \
|
||||
--players 8 \
|
||||
--focus bot-4 \
|
||||
--viewport 1920x1080 \
|
||||
--output /tmp/wowngeon-pvp-ad-8player.webm \
|
||||
--screenshot /tmp/wowngeon-pvp-ad-8player.png \
|
||||
--trace /tmp/wowngeon-pvp-ad-8player.json
|
||||
```
|
||||
|
||||
The recorded eight-player proof and its no-video control both ended with `bot-4` at tick 49 and
|
||||
the same final state hash. This checks that browser recording does not alter the authoritative race.
|
||||
|
||||
## Reproducibility
|
||||
|
||||
The default seed is fixed. A custom seed must be exactly 64 hexadecimal characters:
|
||||
|
||||
```bash
|
||||
NODE_ENV=development npm run capture:pvp -- \
|
||||
--confirm-local-free-only \
|
||||
--seed beb270f3806a97e9ef73c8f83a6eae19a92f90ab38af9ad8b365cb74c41b2702
|
||||
```
|
||||
|
||||
Each bot receives a numbered tick and the same authoritative state over Socket.IO. The server waits
|
||||
for every bot response, queues inputs in stable bot order, and lets the engine resolve them using its
|
||||
committed per-tick action priority before advancing. Bot goals are ruleset-aware but contain no
|
||||
randomness beyond the committed dungeon seed. Missing or malformed input aborts the run instead of
|
||||
silently changing the replay. Every run writes a versioned JSON trace beside the video containing
|
||||
the ruleset, presentation metadata, seed, exact inputs, events, camera configuration, state hashes,
|
||||
placements, scores, and mode-correct outcome.
|
||||
|
||||
The gameplay trace is deterministic, and records that gameplay environment overrides were refused.
|
||||
Encoded WebM bytes can differ across Chromium/FFmpeg versions
|
||||
or host timing, so use the JSON state hashes—not the video checksum—to compare replays.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Immutable release artifacts
|
||||
|
||||
`scripts/deploy/build-release-artifact.sh` is the local, non-activating boundary between a reviewed
|
||||
Git commit and the two production application instances. It produces a **runtime artifact**, not a
|
||||
source or operations bundle. It does not read an environment file, install a package, connect to
|
||||
`deb`, or move either `current` symlink.
|
||||
|
||||
## Build gate
|
||||
|
||||
Commit every intended source and documentation change first. Review that commit and confirm that a
|
||||
secret scanner has not found credentials embedded in otherwise legitimate source files; filename
|
||||
policy alone cannot detect a hard-coded secret. Then run:
|
||||
|
||||
```bash
|
||||
git status --short # must print nothing
|
||||
./scripts/deploy/build-release-artifact.sh # defaults to git-<12-character HEAD prefix>
|
||||
cd dist/releases
|
||||
sha256sum --check <release-id>.tar.gz.sha256
|
||||
```
|
||||
|
||||
The builder fails on any tracked or non-ignored untracked worktree change, tracked symlink/gitlink,
|
||||
runtime `.env` or credential-capable dotfile, unsafe release ID, or existing output. Ignored local
|
||||
files are never read because the archive comes only from the selected commit. The archive contains
|
||||
only `LICENSE`, `html/**`, the runtime subset of `src/**`, and deterministic release metadata.
|
||||
It intentionally contains no `docs/`, `test/`, root `scripts/`, deployment service units, wallet
|
||||
migration tools, environment examples, ad-capture bot, stagenet transfer canary, or development
|
||||
database/smoke helpers. Simulation/Jest sources are also excluded, and the staged package manifest
|
||||
retains only the runnable `start`, `preflight`, and `db:migrate:clone` commands. The guarded
|
||||
`src/scripts/preflight.js` and
|
||||
`src/scripts/migrate-disposable-clone.js` remain available for staging checks.
|
||||
`.release-source-sha256` identifies the prefix-free,
|
||||
runtime-allowlisted `git archive`; the external sidecar identifies the final compressed artifact;
|
||||
`.release-files.sha256` verifies every extracted runtime/metadata file without covering the later
|
||||
`src/node_modules` install.
|
||||
|
||||
Run the complete test suite from the clean source commit **before** building. Tests and operator
|
||||
tools are source/control-plane inputs; they are not production runtime files:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
npm ci --ignore-scripts --no-audit --no-fund
|
||||
npm test
|
||||
cd ..
|
||||
git status --short # must still print nothing
|
||||
./scripts/deploy/build-release-artifact.sh
|
||||
```
|
||||
|
||||
Then test the runtime bytes that will be shipped. In a disposable directory, verify the artifact
|
||||
sidecar and manifest and install only the locked production dependency graph:
|
||||
|
||||
```bash
|
||||
cd <directory-containing-artifact>
|
||||
sha256sum --check <release-id>.tar.gz.sha256
|
||||
tar -xzf <release-id>.tar.gz -C <private-test-directory>
|
||||
cd <private-test-directory>/<release-id>
|
||||
sha256sum --check .release-files.sha256
|
||||
cd src
|
||||
npm ci --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
npm ls --omit=dev
|
||||
```
|
||||
|
||||
Advisory lookup is a separate, explicitly authorized network operation. Do not use `npm audit fix`
|
||||
to mutate a candidate or live release.
|
||||
|
||||
## Production staging gate
|
||||
|
||||
Use the same runtime artifact SHA-256 for `monerogue` and `wownerogue`. Transfer it into a
|
||||
root-controlled staging path and verify the operator-pinned 64-hex digest before extraction. The
|
||||
activation boundary must then:
|
||||
|
||||
1. reject an existing release ID, links/special files, an unexpected top-level directory, or unsafe
|
||||
ownership/path components;
|
||||
2. install only locked production dependencies with
|
||||
`npm ci --omit=dev --ignore-scripts --no-audit --no-fund` while staging is writable;
|
||||
3. run `npm ls --omit=dev`, recheck `.release-files.sha256`, then make the complete release
|
||||
`root:root` and non-writable before it can become `current`;
|
||||
4. restore each fresh pre-deploy dump into a separately named clone database, run
|
||||
`npm run db:migrate:clone` under the exact disposable-target contract in
|
||||
[CLONE_MIGRATIONS.md](./CLONE_MIGRATIONS.md), require its exact manifest proof (currently through
|
||||
migration 040), and run the separately reviewed and hash-pinned fleet copy of
|
||||
`financial-audit.sql` there—the SQL is intentionally not inside the runtime artifact;
|
||||
5. run `npm run preflight` from the candidate with each final protected environment. Preflight is
|
||||
intentionally non-mutating and does not replace database, daemon, or wallet health checks.
|
||||
|
||||
The clone runner is deliberately non-activating; there is still no reviewed activation script. Do
|
||||
not point `current` at a candidate until the clone proof and the complete
|
||||
drain/audit/symlink/health/rollback sequence in `~/src/such-fleet/RUNBOOK-wowngeon.md` are satisfied.
|
||||
|
||||
## Source and operations boundary
|
||||
|
||||
The Git checkout remains the review/test source. `~/src/such-fleet` is the deployment control plane
|
||||
for hash-pinned SQL, service units, backup/firewall jobs, clone validation, activation, and rollback.
|
||||
Neither source-tree deployment scripts nor fleet tools are copied beneath the application release
|
||||
directory. A runtime artifact hash does not attest a separately delivered operations file; record
|
||||
and verify an independent hash for every such file in the fleet change plan.
|
||||
|
||||
## Activation and rollback gate
|
||||
|
||||
Drain public ingress and require zero active and queued games. Take and validate fresh dumps; run the
|
||||
financial audit before and after stopping only the affected app. Preserve and validate the exact
|
||||
predecessor target, switch `current` atomically, then start stagenet first. Require bounded local
|
||||
`/health/live` and `/health/ready`, exact network/money-mode disclosures, public TLS, WebSocket, and
|
||||
leaderboard/intake smoke tests before traffic is restored. The stagenet payout canary is a later,
|
||||
one-shot gate and is never a mainnet command.
|
||||
|
||||
Application rollback is safe only if the predecessor was tested against the *post-migration* clone
|
||||
schema. Migrations are forward-only and run one transaction per file at application startup; a
|
||||
mid-sequence failure can leave earlier files committed. On activation failure, stop the candidate,
|
||||
atomically restore the validated predecessor symlink, start it, and require both the exact target and
|
||||
readiness. Do not automatically restore the database after new writes; keep the dump for an
|
||||
operator-reviewed recovery decision.
|
||||
@@ -46,7 +46,7 @@ Key: `S` ≤ half day · `M` ~1–3 days · `L` ~1 week+ · 🔴 launch-blocker
|
||||
- [x] **4.1 L — Break up `index.js` (substantially).** ✅ Extracted the verify HTML → `views/verifyPage.js` (now HTML-escaped), all 15 admin endpoints → `routes/admin.js`, and the 3 smirk endpoints → `routes/auth.js`, each as DI factories with smoke tests. **index.js: 2029 → 882 lines (-57%).** _Remaining user/public routes left inline — they're interleaved with bootstrap and have no route tests, so further slicing is diminishing-return/higher-risk; the structure + pattern are established for later._
|
||||
- [~] **4.2 M — Kill duplicates (partial).** ✅ Deleted the dead `handlePlayerMove` handler (a footgun that double-stepped the monster + bypassed escape/collision detection) and its orphaned `playerMoveTimestamps` map/cleanup — the live path is `MovementManager`. _Remaining (lower-value/higher-effort): merge `rpcService`+`rpccalls`, unify `payout_address` column widths, address the `QueryValidator` false-positive — deferred._
|
||||
- [⏸] **4.3 M — Config snapshots — DEFERRED (risk > value).** The `process.env` config write-back is ugly but **load-bearing**: it's how DB-persisted hot-reload config reaches the env-based readers — `difficultyConfig` selects the difficulty preset from `process.env.GAME_MODE`, and `debugManager`'s simulated-blocks **safety gate** reads it. A proper `ConfigService` means rewiring those sensitive readers; with no config-reload tests and zero correctness/safety upside, the risk isn't justified right now. Documented rather than rewritten.
|
||||
- [~] **4.4 M — Frontend de-spaghetti (safe wins done).** ✅ Shared `escapeHtml` already added in 0.6. Removed the duplicate `_gameMode` declaration (the later `null` silently shadowed the earlier `'free'`). Removed the dead `SocketHandlers._appendMessage` calls in `smirkAuth.js` (the method never existed; always fell through to the working `.text()` append). Gated `broadcastManager`'s high-volume per-event logs (user-count, chat-message-with-PII, game-update, game-start) behind debug mode. _Deferred: jQuery 3.4.1 upgrade and the nested-`setTimeout` init-ordering rewrite (larger, no frontend tests, higher risk)._
|
||||
- [~] **4.4 M — Frontend de-spaghetti (safe wins done).** ✅ Shared `escapeHtml` already added in 0.6. Removed the duplicate `_gameMode` declaration (the later `null` silently shadowed the earlier `'free'`). Removed the dead `SocketHandlers._appendMessage` calls in `smirkAuth.js` (the method never existed; always fell through to the working `.text()` append). Gated `broadcastManager`'s high-volume per-event logs (user-count, chat-message-with-PII, game-update, game-start) behind debug mode. Upgraded the same-origin vendored jQuery from 3.4.1 to distro-sourced 3.7.1 with a recorded package version and SHA-256. _Deferred: the nested-`setTimeout` init-ordering rewrite and a complete nonce-based CSP migration (both require broader browser regression coverage)._
|
||||
- [x] **4.5 S — Delete stale `test/` scratch files.** ✅ Removed 14 non-jest leftovers (5 `.js` scratch scripts + 9 `.html` debug pages) — confirmed they contained no jest constructs and were referenced nowhere. `test/` is now exclusively the 21 real jest suites.
|
||||
|
||||
## Phase 5 — Test & ops overhaul (🟠 — the launch gate)
|
||||
|
||||
+23
-6
@@ -31,7 +31,7 @@ three decision points:
|
||||
```
|
||||
onExit(room, id) a player reached the exit — end the match? set a winner?
|
||||
onDeath(room) a player died — is the match over?
|
||||
rank(room) final ordering (best-first) for placement + winnerId
|
||||
rank(room) final ordering (best-first) for placement; competitive modes also set winnerId
|
||||
```
|
||||
|
||||
- **`FIRST_TO_EXIT`** is a *verbatim port* of the original race logic, so existing matches are
|
||||
@@ -39,8 +39,11 @@ rank(room) final ordering (best-first) for placement + winnerId
|
||||
- **`LAST_ALIVE`** (PvP): reaching the exit means you *survived* (safe, not an instant win); the match
|
||||
ends when ≤1 contender remains; `rank` orders last-standing → escaped → died-later. Combined with
|
||||
`entities.pvpCombat`, stepping onto a living rival **strikes them down** (attacker holds position).
|
||||
Same-tick intents use a seed-derived priority over stable entrant slots, never packet arrival order;
|
||||
an entrant eliminated earlier in that committed order cannot move or attack post-mortem.
|
||||
- **`HIGH_SCORE`**: no instant win; rank by score at the end.
|
||||
- **`ALL_ESCAPE`** (co-op): ends when everyone alive has escaped.
|
||||
- **`ALL_ESCAPE`** (co-op): ends when everyone alive has escaped, records placements as progress
|
||||
metadata, and deliberately leaves the individual `winnerId` null.
|
||||
|
||||
## Where it plugs in
|
||||
|
||||
@@ -51,9 +54,20 @@ and `entities.pvpCombat`, and takes timing from the ruleset. `MatchEngine` is a
|
||||
(`room.resolveTick()` on an interval).
|
||||
|
||||
**Scope note:** the *live single-player* engine (`src/game/game.js`) is intentionally **not** refactored to
|
||||
consume rulesets (too risky to touch the deployed path) — `solo-classic` is a catalog descriptor only. PvP
|
||||
modes are opt-in via `rulesetId`; the match scheduler still builds `race`, and match mode is gated by
|
||||
`MATCH_ENABLED` (inert by default), so nothing changes until wired.
|
||||
consume rulesets (too risky to touch the deployed path) — `solo-classic` is a catalog descriptor only.
|
||||
Multiplayer rulesets are selected by the server with `MATCH_RULESET_ID`; unknown or solo-only values fail
|
||||
closed to `race`. The active ruleset and safe catalog metadata are advertised to clients, but clients cannot
|
||||
select executable rules. Match mode remains gated by `MATCH_ENABLED` (inert by default).
|
||||
|
||||
When `timing.blockDeadline` is true, `MatchScheduler` offers each advancing canonical header to
|
||||
`MatchManager`. An active room expires only when that header is strictly later than its start
|
||||
header and its active-play `minDurationMs` floor has elapsed. Duplicate polls and same-header
|
||||
starts are idempotent; `blockDeadline: false` leaves the hard ceiling as the only clock-based end.
|
||||
|
||||
For paid multiplayer, the selected ruleset ID is part of the durable entrant freeze at canonical
|
||||
header H. The seed uses only the exact configured future-header hash after that freeze and its
|
||||
required confirmation wait; changing the active
|
||||
ruleset cannot reinterpret a pending entrant set or trigger a server-random fallback.
|
||||
|
||||
## Economy per mode
|
||||
|
||||
@@ -73,8 +87,11 @@ game-balance change, not just wiring.
|
||||
```
|
||||
src/game/rulesets/Ruleset.js defineRuleset + WIN/ECONOMY enums
|
||||
src/game/rulesets/winConditions.js FIRST_TO_EXIT (race) / LAST_ALIVE / HIGH_SCORE / ALL_ESCAPE
|
||||
src/game/rulesets/registry.js built-ins + getRuleset/listRulesets/rulesetFromMatchOpts
|
||||
src/game/rulesets/registry.js built-ins + safe match resolver/catalog helpers
|
||||
src/multiplayer/MatchRoom.js consumes the ruleset; PvP combat; delegates win/lose/rank
|
||||
src/multiplayer/MatchEngine.js tick driver
|
||||
src/network/matchScheduler.js advancing-header deadline dispatch
|
||||
src/network/matchManager.js active-play floor + idempotent expiry
|
||||
src/game/difficultyConfig.js world presets (the ruleset's `world` half)
|
||||
src/migrations/029_match_ruleset_id.sql persists the selected ruleset on each match
|
||||
```
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# XMR stagenet financial canary
|
||||
|
||||
`src/scripts/stagenet-financial-canary.js` is the release-gate harness for Monerogue's two paid solo settlement paths. It is intentionally narrow: one new anonymous user, one invoice, at most one funding transfer, one bounded game, and one payout. It supports these scenarios:
|
||||
|
||||
| `E2E_SCENARIO` | Entry | Required outcome | Exact payout |
|
||||
|---|---|---|---|
|
||||
| `direct-2x` | one `single_game` invoice | escape without treasure | invoice amount × 2 |
|
||||
| `credits-3x` | one pure credits-package invoice, then one `PAID_CREDITS` entry | collect treasure, then escape | configured credits payout base × 3 |
|
||||
|
||||
Stagenet XMR is valueless test currency. Never send mainnet XMR to a stagenet address. This harness refuses mainnet-facing application state and funding-wallet addresses, but that guard is not a substitute for checking the wallet and daemon commands yourself.
|
||||
|
||||
## Hard safety properties
|
||||
|
||||
The harness fails closed unless all of these are true:
|
||||
|
||||
- The application URL explicitly names `127.0.0.1`, `localhost`, or `::1` on dedicated port `3102`. It cannot target the public router, LAN hostname, or Nebula address.
|
||||
- Readiness, public disclosures, REST mode data, and Socket.IO mode data all identify real XMR stagenet with payments and payout dispatch enabled.
|
||||
- Both paid solo modes advertise exact 2× escape and 3× treasure multipliers.
|
||||
- The current `/api/disclosures` response requires the canonical five-field acknowledgement. The harness refetches it immediately before the value-bearing action and echoes exactly `policyVersion`, `ageEligible`, `termsRead`, `riskAccepted`, and `testnetUnderstood`.
|
||||
- The selected PostgreSQL database name contains `canary`, `e2e`, and the selected scenario (`direct` or `credits`). Its connection must be localhost, the harness session is read-only, current financial migrations must exist, and the database must contain only the migration-seeded admin with no game or financial rows.
|
||||
- Before any invoice, funding-wallet call, or transfer, a nonce challenge binds the application pool to the harness connection's exact PostgreSQL cluster identifier, database OID, and database name. A same-named database on another cluster does not pass.
|
||||
- The funding wallet RPC is localhost-only and either digest-authenticated or enabled by a separate explicit unauthenticated-local-RPC acknowledgement.
|
||||
- The funding wallet validates both its own address and the invoice as stagenet. `get_address_index` must prove the invoice does not belong to the funding wallet; this prevents a meaningless house-wallet self-payment.
|
||||
- Live execution needs three exact confirmations plus an atomic-unit transfer ceiling. The transfer gate is marked used before the sole `transfer` RPC call, so an ambiguous transport failure is never retried.
|
||||
- The bot is bounded to one dungeon depth, 3,000 moves, and eight minutes. The direct scenario treats treasure as blocked. The credits scenario treats the exit as blocked until treasure is collected.
|
||||
- Wallet addresses, session tokens, proof seeds, transaction hashes, and credentials are never printed. Failure messages redact those shapes.
|
||||
|
||||
The harness never creates, restores, exports, migrates, or backs up a wallet. It never deploys code, changes router state, or edits fleet configuration.
|
||||
|
||||
## Required topology
|
||||
|
||||
Use two different stagenet wallets:
|
||||
|
||||
1. The canary application owns the normal house wallet used to create invoice subaddresses and dispatch payouts.
|
||||
2. A separate, low-balance funding wallet sends the entry/package payment and receives the payout.
|
||||
|
||||
Expose only the separate funding wallet RPC to the harness, on loopback. Do not give the harness the house-wallet RPC URL or credentials. Keep the funding balance no larger than needed for the selected invoice plus fees.
|
||||
|
||||
Before live use, independently verify that both wallet files are recoverable according to the operator's wallet-backup procedure. Do not put seeds, private keys, wallet passwords, transaction keys, or mnemonic output in this runbook, shell history, screenshots, CI variables, or canary logs. This harness deliberately does not inspect or print seed material.
|
||||
|
||||
## Build the disposable canary instance
|
||||
|
||||
Run the exact release candidate intended for production, but bind it only to localhost port `3102`. Use a dedicated PostgreSQL database for each scenario; do not run both scenarios in the same database and never reuse a database after any live attempt.
|
||||
|
||||
Recommended game-profile overrides for the one-shot financial check are:
|
||||
|
||||
```dotenv
|
||||
DIFFICULTY_PRESET=easy
|
||||
MONSTER_SPEED=0
|
||||
DUNGEON_LEVELS=1
|
||||
```
|
||||
|
||||
`MONSTER_SPEED=0` leaves the verifiable monster placement in the committed dungeon but prevents movement during the canary. The harness verifies the one-level state; the exact `E2E_CANARY_PROFILE` confirmation below is the operator's assertion that the easy/static-monster configuration is the one actually running.
|
||||
|
||||
The canary instance must otherwise use the intended production money configuration, including:
|
||||
|
||||
- `CRYPTO_TYPE=XMR`, `MONERO_NETWORK=stagenet`, real stagenet daemon and house wallet RPC;
|
||||
- direct and credits paid modes enabled together;
|
||||
- direct and credits payouts enabled;
|
||||
- 2× escape and 3× treasure multipliers for both modes;
|
||||
- a positive `CREDITS_PAYOUT_BASE` whose 3× result is within the configured payout bounds;
|
||||
- a credits-only package (default id `small`) with no race entries, packs, or premium grant;
|
||||
- production paid-action acknowledgement enabled and current operator/legal disclosure metadata.
|
||||
|
||||
Use a fresh database name such as:
|
||||
|
||||
```text
|
||||
monerogue_canary_direct_e2e
|
||||
monerogue_canary_credits_e2e
|
||||
```
|
||||
|
||||
First boot the release candidate against the new database with payout dispatch disabled solely to run startup migrations. Stop it after readiness. Then run database preflight. Finally boot the same release candidate and same database with the intended payout configuration on localhost port `3102`. Do not point a public router at this instance.
|
||||
|
||||
Enable the identity endpoint only on that final isolated canary boot:
|
||||
|
||||
```dotenv
|
||||
NODE_ENV=production
|
||||
PORT=3102
|
||||
CANARY_DATABASE_HANDSHAKE=I_AM_AN_ISOLATED_XMR_STAGENET_CANARY
|
||||
CANARY_EXPECT_DATABASE=monerogue_canary_direct_e2e
|
||||
CANARY_DATABASE_NONCE_FILE=/run/credentials/canary/database-nonce
|
||||
```
|
||||
|
||||
Use the corresponding `credits` database name for that scenario. If none of the three
|
||||
`CANARY_DATABASE_*` variables is set, the endpoint is not registered. A partial configuration,
|
||||
non-XMR-stagenet process, non-production process, wrong port, or `DB_NAME` mismatch aborts startup.
|
||||
The endpoint signs its database identity and a fresh caller challenge; it never returns the nonce.
|
||||
It also rejects any TCP peer that is not loopback, without trusting proxy-forwarded client headers.
|
||||
|
||||
## Protect local secret files
|
||||
|
||||
Credential files read by the harness must be regular files, not symlinks, and have no group/world permission bits (for example mode `0600` or `0400`). Prefer file-backed variables:
|
||||
|
||||
```text
|
||||
E2E_FUNDING_RPC_USER_FILE=/run/credentials/canary/funding-rpc-user
|
||||
E2E_FUNDING_RPC_PASSWORD_FILE=/run/credentials/canary/funding-rpc-password
|
||||
E2E_DATABASE_URL_FILE=/run/credentials/canary/database-url
|
||||
E2E_DATABASE_NONCE_FILE=/run/credentials/canary/database-nonce
|
||||
```
|
||||
|
||||
Create a new random 32-byte nonce for each scenario and expose the same protected file to the
|
||||
application (`CANARY_DATABASE_NONCE_FILE`) and harness (`E2E_DATABASE_NONCE_FILE`). Do not put its
|
||||
contents in an environment variable, command argument, log, or screenshot. Delete the nonce after
|
||||
the isolated canary is stopped; it is not a wallet key or recovery artifact.
|
||||
|
||||
Alternatively, `E2E_DATABASE_ENV_FILE` may point to a protected dotenv file containing `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, and `DB_PASSWORD`. Set exactly one database source. `DB_HOST` and database URLs must explicitly use loopback.
|
||||
|
||||
## Read-only preflights
|
||||
|
||||
The public preflight performs no Socket.IO session registration, invoice creation, database mutation, or wallet RPC call:
|
||||
|
||||
```bash
|
||||
E2E_MODE=preflight \
|
||||
E2E_TARGET=http://127.0.0.1:3102 \
|
||||
npm --prefix src run canary:stagenet
|
||||
```
|
||||
|
||||
The database preflight opens only a read-only PostgreSQL session. Run it while the freshly migrated database still contains no canary session:
|
||||
|
||||
```bash
|
||||
E2E_MODE=database-preflight \
|
||||
E2E_SCENARIO=direct-2x \
|
||||
E2E_EXPECT_DATABASE=monerogue_canary_direct_e2e \
|
||||
E2E_DATABASE_URL_FILE=/run/credentials/canary/direct-database-url \
|
||||
npm --prefix src run canary:stagenet
|
||||
```
|
||||
|
||||
For the credits scenario, use `E2E_SCENARIO=credits-3x`, a database name containing `credits`, and its separate protected connection file.
|
||||
|
||||
## Live direct 2× escape
|
||||
|
||||
Review the configured direct entry atomic amount before setting `E2E_MAX_TRANSFER_ATOMIC`. The ceiling must be at least the advertised invoice amount and should equal that amount when practical.
|
||||
|
||||
```bash
|
||||
E2E_MODE=live-stagenet \
|
||||
E2E_SCENARIO=direct-2x \
|
||||
E2E_TARGET=http://127.0.0.1:3102 \
|
||||
E2E_EXPECT_DATABASE=monerogue_canary_direct_e2e \
|
||||
E2E_DATABASE_URL_FILE=/run/credentials/canary/direct-database-url \
|
||||
E2E_DATABASE_NONCE_FILE=/run/credentials/canary/database-nonce \
|
||||
E2E_FUNDING_RPC_URL=http://127.0.0.1:38083 \
|
||||
E2E_FUNDING_RPC_USER_FILE=/run/credentials/canary/funding-rpc-user \
|
||||
E2E_FUNDING_RPC_PASSWORD_FILE=/run/credentials/canary/funding-rpc-password \
|
||||
E2E_MAX_TRANSFER_ATOMIC=REPLACE_WITH_REVIEWED_ATOMIC_CEILING \
|
||||
E2E_FEE_CUSHION_ATOMIC=1000000000 \
|
||||
E2E_CONFIRM=I_UNDERSTAND_THIS_BROADCASTS_ONE_XMR_STAGENET_TRANSFER \
|
||||
E2E_SCENARIO_CONFIRM=DIRECT_2X_ESCAPE \
|
||||
E2E_CANARY_PROFILE=EASY_STATIC_MONSTER_ONE_LEVEL \
|
||||
npm --prefix src run canary:stagenet
|
||||
```
|
||||
|
||||
The harness binds a fresh fairness offer to the direct invoice before revealing its address, transfers the exact invoice amount, waits for receipt-backed confirmation and automatic game start, avoids treasure, escapes, and verifies an exact 2× payout.
|
||||
|
||||
## Live credits-package then 3× treasure escape
|
||||
|
||||
Review the selected package's atomic price and set the ceiling accordingly. The default package id is `small`; override it with `E2E_CREDITS_PACKAGE_ID` only after confirming it is a pure credits-only package.
|
||||
|
||||
```bash
|
||||
E2E_MODE=live-stagenet \
|
||||
E2E_SCENARIO=credits-3x \
|
||||
E2E_CREDITS_PACKAGE_ID=small \
|
||||
E2E_TARGET=http://127.0.0.1:3102 \
|
||||
E2E_EXPECT_DATABASE=monerogue_canary_credits_e2e \
|
||||
E2E_DATABASE_URL_FILE=/run/credentials/canary/credits-database-url \
|
||||
E2E_DATABASE_NONCE_FILE=/run/credentials/canary/database-nonce \
|
||||
E2E_FUNDING_RPC_URL=http://127.0.0.1:38083 \
|
||||
E2E_FUNDING_RPC_USER_FILE=/run/credentials/canary/funding-rpc-user \
|
||||
E2E_FUNDING_RPC_PASSWORD_FILE=/run/credentials/canary/funding-rpc-password \
|
||||
E2E_MAX_TRANSFER_ATOMIC=REPLACE_WITH_REVIEWED_ATOMIC_CEILING \
|
||||
E2E_FEE_CUSHION_ATOMIC=1000000000 \
|
||||
E2E_CONFIRM=I_UNDERSTAND_THIS_BROADCASTS_ONE_XMR_STAGENET_TRANSFER \
|
||||
E2E_SCENARIO_CONFIRM=CREDITS_PACKAGE_THEN_3X_TREASURE_ESCAPE \
|
||||
E2E_CANARY_PROFILE=EASY_STATIC_MONSTER_ONE_LEVEL \
|
||||
npm --prefix src run canary:stagenet
|
||||
```
|
||||
|
||||
The harness purchases the package, verifies the exact confirmed grant and balance update, consumes a new fairness offer in `auto_start`, spends exactly one configured game credit, collects treasure before allowing the exit, and verifies an exact 3× payout based on `CREDITS_PAYOUT_BASE` (not the package price).
|
||||
|
||||
## Exact settlement assertions
|
||||
|
||||
After the funding wallet observes the incoming payout, the harness checks the read-only database connection. A passing run has exactly:
|
||||
|
||||
- two users: the migration-seeded admin and one canary identity;
|
||||
- one confirmed native-Monero payment with `received_amount == expected_amount` and valid confirmation evidence;
|
||||
- one unique confirmed `chain_output` receipt whose amount exactly equals the invoice;
|
||||
- one won/escaped game with consumed entry evidence, committed 2×/3× immutable payout terms, the selected paid mode, matching fairness v2 identity, and the expected treasure flag;
|
||||
- one completed payout with the exact amount, multiplier, reason, payout address, and transaction evidence;
|
||||
- no refunds, late reviews, matches, match queues, race-entry ledgers, or race-entry lots.
|
||||
|
||||
The direct scenario additionally requires exactly the `direct_entry +1` and `game_entry -1` credit-ledger rows, a zero balance, unified purchase progress of one, the game linked to the fairness-bound payment, and no product-entitlement marker.
|
||||
|
||||
The credits scenario additionally requires exactly one payment-linked `package_purchase` grant, one `game_entry` debit, the exact final balance, one active payment-entitlement marker matching the advertised grant, and a `PAID_CREDITS` game with no direct `payment_id`.
|
||||
|
||||
## Failure handling
|
||||
|
||||
Before the line `one exact, non-retriable XMR stagenet transfer was broadcast`, a failure means no funding transfer was attempted. Correct the configuration, recreate a fresh scenario database if any app session was made, and repeat the preflights.
|
||||
|
||||
At or after that line, never rerun the command and never reuse the database. A transport timeout can be ambiguous even when the wallet accepted the transfer. Preserve the database and both wallet files, stop the isolated canary, and reconcile the invoice receipt, payment, game, payout, and wallet histories manually. The harness intentionally performs no automatic retry, refund, database cleanup, wallet cleanup, or destructive recovery.
|
||||
|
||||
Only after both scenarios pass on separate databases should the release be considered financially canary-tested. Passing does not authorize deployment; deployment and router/fleet changes remain separate reviewed operations.
|
||||
|
||||
## Local verification (no network or wallet use)
|
||||
|
||||
```bash
|
||||
node --check src/scripts/stagenet-financial-canary.js
|
||||
npm --prefix src test -- --runTestsByPath ../test/canaryDatabaseIdentity.test.js ../test/stagenetFinancialCanary.test.js
|
||||
npm --prefix src run canary:stagenet -- --help
|
||||
```
|
||||
|
||||
These commands parse the harness, run its pure/static safety tests, and print help. They do not contact the application, database, or either wallet.
|
||||
@@ -1,18 +1,18 @@
|
||||
# Tavern & Multiplayer — Design & Build Plan
|
||||
|
||||
Match mode (multiplayer races) now has its own dedicated design doc: see `docs/MATCH_MODE.md`.
|
||||
Match mode has its own dedicated design doc: see `docs/MATCH_MODE.md`.
|
||||
|
||||
**Status:** Tavern is complete; multiplayer race mode is implemented (see `docs/MATCH_MODE.md`).
|
||||
PvP combat remains a future extension. Both modes are disabled by default and gated by config, so
|
||||
existing single-player play is unaffected.
|
||||
**Status:** Tavern and server-selected multiplayer rulesets (`race`, `last-alive`, `score-attack`,
|
||||
`coop-escape`) are implemented (see `docs/MATCH_MODE.md`). Both surfaces are disabled by default
|
||||
and gated by config, so existing single-player play is unaffected.
|
||||
|
||||
## Shared engine: one Room, two modes
|
||||
|
||||
Both modes are built on a single shared component — a real-time, multi-occupant `Room`:
|
||||
|
||||
- **Tavern** — a peaceful Room: players pick an avatar, walk around, chat, and watch live games.
|
||||
- **Multiplayer** — the same Room engine with a dungeon, monsters, hazards, and player-vs-player
|
||||
combat added.
|
||||
- **Multiplayer** — the same Room engine with a dungeon, monsters, and ruleset-selected
|
||||
collision/combat behavior.
|
||||
|
||||
The engine (`src/multiplayer/`) has no Socket.IO, database, or Wownerogue coupling. A manager
|
||||
layer owns transport (Socket.IO rooms, broadcasts) and the server-tick timer. Keeping the engine
|
||||
@@ -24,35 +24,36 @@ transport-agnostic makes it unit-testable in isolation and reusable as a standal
|
||||
choose an avatar, move around a shared map, and chat. A spectator camera lets them watch live
|
||||
single-player or multiplayer games from inside the room.
|
||||
|
||||
**Multiplayer Race** — a shared match Room. Players race to escape a dungeon before the next block
|
||||
and contend with a shared monster. Match rooms enable occupant collision; the Tavern does not.
|
||||
PvP combat is planned for a later milestone.
|
||||
**Multiplayer Match** — a shared dungeon Room using the operator-selected ruleset. Players may race
|
||||
to escape, fight to remain last alive, compete for score, or cooperate. Block-bounded rulesets end
|
||||
on the first advancing header after their active-play duration floor; every match also has a hard
|
||||
ceiling. Match rooms enable the collision/combat rules selected by the ruleset; the Tavern does not.
|
||||
|
||||
## Operator Policy
|
||||
|
||||
The server operator controls which modes are enabled and, for multiplayer, the economic model.
|
||||
Configuration is driven through the existing `config/` layer (DB-backed, hot-reloadable) rather
|
||||
than static env vars alone, so operators can adjust settings without a restart. A later extension
|
||||
allows per-room settings.
|
||||
Mode activation, the multiplayer ruleset, and multiplayer economy switches are startup environment
|
||||
configuration. Change `SOLO_ENABLED`, `TAVERN_ENABLED`, `MATCH_ENABLED`, `MATCH_RULESET_ID`, or the
|
||||
`MATCH_*` economy settings in the deployment configuration and restart the service. The separate
|
||||
database-backed `ConfigPersistence` allowlist covers a limited set of solo payment/difficulty
|
||||
values; it does not hot-reload match modes or rulesets. There is no per-room ruleset editor today.
|
||||
|
||||
Which modes are enabled is surfaced to clients in the `game_mode_info` event as
|
||||
`modes: { solo, tavern, multiplayer }` (from `SOLO_ENABLED` / `TAVERN_ENABLED` /
|
||||
`MULTIPLAYER_ENABLED`). The client shows or hides entry points from this, so any single mode can
|
||||
run on its own — including a Tavern-only instance. Solo is on unless explicitly disabled;
|
||||
Tavern and Multiplayer are opt-in.
|
||||
`modes: { solo, tavern, match }` (from `SOLO_ENABLED`, `TAVERN_ENABLED`, and `MATCH_ENABLED`).
|
||||
The client shows or hides entry points from this, so any single mode can run on its own — including
|
||||
a Tavern-only instance. Solo is on unless explicitly disabled; Tavern and Match are opt-in.
|
||||
|
||||
Multiplayer economic models (operator-selected per instance):
|
||||
|
||||
| Model | Description |
|
||||
|---|---|
|
||||
| Free | No entry cost or payout. |
|
||||
| Entry fee, leaderboard only | Players pay to enter; results affect a leaderboard, no payout. |
|
||||
| Winner-take-pot | Entrants stake into a pot awarded to the winner. |
|
||||
| Even-return | Entrants recover their stake; only a flat fee is deducted. |
|
||||
| `free` | No entry cost or payout; competitive results use the Free/Pleb board. |
|
||||
| `credits_prestige` | Spend credits; competitive results use the separate PvP Prestige board, with no crypto payout. |
|
||||
| `crypto_race` | Use a backed race-entry ticket; an eligible competitive winner receives the disclosed pot less the configured fee. |
|
||||
|
||||
The default configuration is conservative (free, no payout), matching the current wownerogue
|
||||
instance. Operators are responsible for ensuring their chosen configuration complies with
|
||||
applicable laws in their jurisdiction.
|
||||
The baseline defaults are conservative (free, no payout). Instance deployment configuration may
|
||||
enable additional economies; operators are responsible for ensuring their selected configuration
|
||||
complies with applicable laws in their jurisdiction.
|
||||
|
||||
## Avatars
|
||||
|
||||
@@ -147,7 +148,8 @@ can be swapped later without changing callers. The current backend remains the d
|
||||
|
||||
Rendering goes through a shared, renderer-agnostic **scene model**: game/tavern state is adapted
|
||||
into a `Scene` (a tile grid + entities), and any renderer draws it. This lives in
|
||||
`html/js/render/` and is used by both the Tavern and (planned) the main game.
|
||||
`html/js/render/` and is used by the Tavern, multiplayer client, spectators, and the single-player
|
||||
render bridge.
|
||||
|
||||
Render tiers:
|
||||
- **ASCII** — monospace glyph grid (canvas). Always-available fallback; accessible.
|
||||
@@ -201,22 +203,21 @@ Tavern:
|
||||
- **T1** — `TavernManager`: Socket.IO room, server-tick timer, join/move/leave broadcasts; minimal
|
||||
client (`html/tavern.html`) rendering avatars; gated behind `TAVERN_ENABLED`. (Done)
|
||||
- **T2a** — `ChatProvider` seam + tavern chat. (Done)
|
||||
- **T2b** — Spectator camera into live games from the Tavern.
|
||||
- **T3** — Avatar picker with policy-gated unlocks.
|
||||
- **T2b** — Spectator camera into live solo and multiplayer games from the Tavern. (Done)
|
||||
- **T3** — Avatar picker with policy-gated unlocks. (Done)
|
||||
|
||||
Multiplayer:
|
||||
- **M0** — Match Room with a dungeon and shared monsters; players race the block. Includes a load
|
||||
test with many simulated clients to measure server-tick performance under concurrency.
|
||||
- **M1** — Player-vs-player combat.
|
||||
- **M0** — Match Room with a dungeon, shared monster, server ticks, and bounded lifecycle. (Done)
|
||||
- **M1** — Player-vs-player combat for the `last-alive` ruleset. (Done)
|
||||
- **M2** — Environmental hazards.
|
||||
- **M3** — Operator Policy economic models wired in.
|
||||
- **M3** — Free, credit-prestige, and gated crypto-race economies wired in. (Done)
|
||||
|
||||
Rendering:
|
||||
- **R0** — Shared render kit: scene model + ASCII / Tiled / Fancy (PixiJS) renderers + mode
|
||||
switch, wired to the Tavern. Premium-mode gating scaffolded. (Done)
|
||||
- **R1** — Wire the main game to the render kit (refactor the `display/` layer behind it).
|
||||
- **R1** — Wire the main game to the render kit behind the existing display layer. (Done)
|
||||
- **R2** — Real tilesets / sprite sheets and the Fancy-ASCII tier.
|
||||
- **R3** — Wire premium render modes to credit entitlements (Operator Policy).
|
||||
- **R3** — Wire premium render modes to credit entitlements (Operator Policy). (Done)
|
||||
|
||||
All milestones are additive and config-gated; the single-player path is unchanged.
|
||||
|
||||
@@ -232,18 +233,18 @@ All milestones are additive and config-gated; the single-player path is unchange
|
||||
and Socket.IO room broadcasts. Wired into `SocketHandlers` (`tavern_join` / `tavern_move` /
|
||||
`tavern_leave`, plus disconnect cleanup and shutdown). Inert unless `TAVERN_ENABLED=true`.
|
||||
|
||||
`html/tavern.html` — a minimal self-contained client: connect, choose a name/avatar, enter the
|
||||
room, walk with arrow keys / WASD, and chat. Renders the map and occupants on a canvas.
|
||||
`html/tavern.html` — the browser client: connect, choose a name/avatar, enter the room, walk with
|
||||
keyboard or on-screen controls, chat, watch solo/multiplayer games, or join a match.
|
||||
|
||||
`src/network/chat/` — the `ChatProvider` seam. `ChatProvider` (interface) + `SocketChatProvider`
|
||||
(default: Socket.IO delivery + Postgres history). Both the global chat (`ChatHandler`) and tavern
|
||||
chat deliver through it, so the backend can be swapped (e.g. a Nostr channel) without changing
|
||||
callers. Tavern chat is room-scoped and ephemeral.
|
||||
|
||||
`html/js/render/` — the render kit: `sceneModel.js` (renderer-agnostic scene + tavern adapter),
|
||||
`asciiRenderer.js`, `tileRenderer.js`, `fancyRenderer.js` (PixiJS), and `renderModes.js`
|
||||
(registry, factory with graceful fallback, entitlements, persistence). The tavern client uses it
|
||||
with a mode toolbar; premium modes are marked and gated by `RK.entitlements`.
|
||||
`html/js/render/` — the render kit: `sceneModel.js` (renderer-agnostic adapters), ASCII, tiled,
|
||||
Fancy (PixiJS), isometric, and Three.js renderers, plus `renderModes.js` (registry, factory with
|
||||
graceful fallback, entitlements, persistence). Clients expose a mode toolbar where applicable;
|
||||
premium modes are marked and gated by `RK.entitlements`.
|
||||
|
||||
Tests: `test/tavernRoom.test.js` (engine), `test/tavernManager.test.js` (manager incl. chat),
|
||||
`test/chatProvider.test.js`, `test/renderScene.test.js` (scene adapter),
|
||||
|
||||
+5
-2
@@ -28,8 +28,11 @@ Done:
|
||||
|
||||
Pending:
|
||||
- [ ] **Multi-level calibration run**: lock per-network level counts against a target house-win (sim multi-level support is in; counts are currently starting points).
|
||||
- [ ] **Block-bounded pot race** (flagship): consume `Ruleset.timing.blockDeadline`; add pot+rake economy (house = zero-risk rake-taker). Decisions pending: empty-pot rule, prize split.
|
||||
- [ ] **PvP wiring**: connect the already-built `last-alive`/`score-attack` rulesets to a real queue.
|
||||
- [x] **Block-bounded pot race** (flagship): `Ruleset.timing.blockDeadline` expires active matches
|
||||
on the first advancing header after the play-time floor; winner-take-pot-minus-fee admission
|
||||
is durable and co-op is excluded until split-payout semantics exist.
|
||||
- [x] **PvP wiring**: the server selects `race`, `last-alive`, `score-attack`, or `coop-escape`
|
||||
with `MATCH_RULESET_ID`; match state and persistence record the active ruleset.
|
||||
- [ ] **Hazard gameplay**: generator places lava/poison/spikes (FX already renders them) + damage stakes; escalate per level.
|
||||
- [ ] **Client polish**: "Level N/M" indicator + "descended" toast (server sends `depth`/`maxDepth` + `game_event: descend`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user