Fixes the two High-severity findings from Isaac Hague's 19/08/2026 review (F-01, F-02) plus F-03 through F-09: - F-01: content writes are now sanitised server-side (sanitize-html) as the real security boundary — the browser-side sanitiser is UX, not enforcement, and a direct API write bypassed it entirely. Also closes the javascript: href gap in sanitiseInline(). - F-02: refusing the factory admin password no longer depends on NODE_ENV; it's the unconditional default now, with an explicit ALLOW_DEV_PASSWORD=1 opt-in for local dev. - F-03: adds CSP (report-only — the legacy inline onclick="" handlers would break under enforcement) and HSTS, in both server/index.mjs and vercel.json. - F-04/F-05: rate-limits /api/leads and rotates leads.jsonl past 5MB; CSV export neutralises leading =+-@ so exports can't carry live formulas. - F-06: sessions drop from 12h to 4h and are tied to a per-boot random epoch, so a restart now actually revokes outstanding tokens. - F-07/F-08/F-09: generic messages on 5xx, fixed-length password comparison (no more length disclosure via the short-circuit), periodic throttle-map cleanup. F-10 (dependency advisories): applied the two non-breaking patches (nanoid, postcss); the vite/react-router-dom major bumps are left for a separate pass, per the report's own recommendation. F-11 (PDF Content-Disposition) and a CAPTCHA/honeypot on the enquiry form are deliberately left open — both are product/UX calls, not pure security fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.1 KiB
Deployment and operations
The app ships as a static build plus one small Node process. The Node process is
what makes /admin able to publish, so host it wherever the CMS is needed.
npm ci
npm run build # → dist/
ADMIN_PASSWORD='…' npm start # serves dist/ + the API on port 8787
Environment
| Variable | Default | Purpose |
|---|---|---|
ADMIN_PASSWORD |
— | Password for /admin. Required — the server refuses to start without it (or with the factory password blackdice). |
ALLOW_DEV_PASSWORD |
unset | Local development only: accept the factory password blackdice instead of requiring ADMIN_PASSWORD. Never set in a real deployment. |
ADMIN_SECRET |
derived from the password | Signs session tokens. Set it to invalidate all sessions independently of the password (a server restart also invalidates every session, regardless of this setting). |
PORT |
8787 |
Listen port. |
CONTENT_DIR |
./content |
Published content, uploads, snapshots, enquiries. Put this on persistent storage. |
DIST_DIR |
./dist |
The built site. |
SITE_URL |
https://www.blackdice.ai |
Canonical origin used in sitemap.xml. |
Sessions last 4 hours. Failed logins are throttled at 10 per IP per 15 minutes;
enquiry submissions at 8 per IP per 10 minutes. Uploads are capped at 32MB and
limited to images, MP4/WebM and PDF. leads.jsonl rolls over to a .bak file
once it passes 5MB rather than growing without bound.
What lives where
content/
site-content.json the published site: copy, articles, demos, settings
versions/ one snapshot per publish (latest 60 kept)
uploads/ images and clips uploaded through Studio
leads.jsonl enquiry form submissions
dist/ the built site (safe to delete and rebuild)
public/content/posts/ article heroes imported from the old studio (in git)
Back up content/. It is the only thing that cannot be rebuilt from the repo.
A nightly copy of the directory is enough; site-content.json is a single small
JSON file. Editors can also take their own backup from Studio → History → Download
this draft.
Behind IIS or nginx
Serve the app from Node and reverse-proxy to it — that keeps /api, uploads, the
SPA fallback and the live sitemap working with no extra configuration.
nginx:
location / {
proxy_pass http://127.0.0.1:8787;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 40M; # Studio uploads
}
IIS: install URL Rewrite + ARR and proxy the site to http://127.0.0.1:8787, or run
it under iisnode. Raise maxAllowedContentLength to ~40MB for uploads.
Keep the process alive with a Windows service (nssm), pm2, or systemd:
# /etc/systemd/system/blackdice-site.service
[Service]
WorkingDirectory=/srv/blackdice
Environment=ADMIN_PASSWORD=…
Environment=CONTENT_DIR=/srv/blackdice-content
ExecStart=/usr/bin/node server/index.mjs
Restart=always
Docker:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
ENV PORT=8787 CONTENT_DIR=/data
VOLUME /data
EXPOSE 8787
CMD ["node", "server/index.mjs"]
Static hosting without the CMS
dist/ can be served by any static host. Deep links need an SPA fallback, which is
already configured for Vercel (vercel.json), Netlify (public/_redirects) and IIS
(public/web.config). Apache:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
In that setup the site shows the content compiled into the build and /admin cannot
publish. To make a change, run Studio locally (npm run dev), publish, then commit
the resulting content/site-content.json and any content/uploads/ files and
redeploy — the build copies neither automatically, so they must be placed alongside
dist/ (dist/content/site-content.json, dist/content/uploads/…).
Release checklist
npm ci && npm run build— the build fails on type errors, so it gates itself.buildalso regeneratespublic/sitemap.xml/robots.txtfrom whatever is incontent/site-content.json(falling back to factory content) beforevite buildcopiespublic/intodist/, so the static sitemap can't go stale relative to the last build. SetSITE_URLif it isn'thttps://www.blackdice.ai.- Deploy
dist/,server/,package.json, and keepcontent/in place. - Check:
/,/mobile-sdk,/news, one/blog/<slug>deep link,/sitemap.xml,/adminlogin. - In Search Console, submit
https://www.blackdice.ai/sitemap.xmland request indexing for the product pages — they now have their own URLs to index.
After the first deploy
- Set
ADMIN_PASSWORDand share it only with the people who edit the site. - Publish once from Studio so
content/site-content.jsonexists and snapshots begin. - Approve and publish the GSMA Open Gateway release (Studio → Articles).
- Upload the Retina and Angel walkthrough clips (Studio → Demos) to finish the Halo CPE demos.