Files
qapi/asset/nginx/qapi.blackbeard.observer.conf
grenadeandClaude Opus 5 99483d90ca feat: rename to the qapi console, and deploy it to oolon
The console is now **qapi console**, served at https://qapi.blackbeard.observer.
The papi logo and the upstream licence stay: this is a fork of
polkadot-api/papi-console, not a rewrite, and the sidebar link points at our
fork rather than theirs.

Deployment follows architecture/deployment-gitea-actions.md — the workflow is
the source of infra truth, and one-time host provisioning is an operator script:

  script/infra-setup.sh   dns  -> the Cloudflare CNAME to bl.thgttg.com
                          cert -> Let's Encrypt, DNS-01, ECDSA
                          edge -> gitea_ci, scoped sudoers, webroot, vhosts

  .gitea/workflows/deploy.yaml   build (pnpm) -> rsync the bundle -> reload

The roles run in that order because certbot uses a DNS-01 challenge and nginx
fails its config test on a missing ssl_certificate — which would block every
reload on a SHARED proxy, not just this vhost. The same reasoning bounds what CI
may do: the sudoers drop-in grants rsync into one webroot, restorecon on it,
a config test and a reload. No certbot, no /etc/letsencrypt, no sites-available
write, no useradd.

Two vhosts, per architecture/reverse-proxies.md: the public one on the https
tier at 127.0.0.1:14443 behind the stream SNI router that owns TCP 443 (binding
443 directly is undetectable by `nginx -t` and ends with nginx silently serving
stale certificates), and a qapi.internal one for mesh clients, which stays
disabled until someone mints its internal certificate.

There is no application host and no upstream: the console is a static SPA that
opens WebSockets straight from the browser to the chains' own RPC endpoints.

The build gate is `pnpm build` (tsc -b + vite build) plus a check that the
signers-common patch is applied to the copy node actually resolves. Without that
patch every signing attempt dies with `Unkown signer` in a browser, after
deploy, in front of a user — and a lockfile drift is all it would take.

`pnpm lint` is deliberately not in the gate: it is broken upstream and was
before this fork touched anything (typescript-eslint 8.69 refuses to load
against the TypeScript 7.0 this repo resolves).

Live and verified: the public name answers 200 with this console's title over
the WAN address, serving the Let's Encrypt certificate for it, and the whole
deploy path — rsync as gitea_ci through the scoped sudoers, restorecon, config
test, reload, fetch — has been run by hand end to end.

Refs #3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 08:42:06 +03:00

71 lines
3.2 KiB
Plaintext

# Public vhost for the qapi console, on the oolon edge proxy (kosherinata).
#
# Cert: Let's Encrypt via certbot + Cloudflare DNS-01 (architecture/external-tls.md).
# Installed by script/infra-setup.sh --role edge, never by CI — the runner has no
# rights to read certificate keys or reload nginx on a shared edge proxy. CI only
# rsyncs the built bundle into the webroot.
#
# ln -sf ../sites-available/qapi.blackbeard.observer.conf /etc/nginx/sites-enabled/
#
# listen 127.0.0.1:14443 + proxy_protocol, NOT 443: an nginx stream SNI router
# owns TCP 443 on this host and hands every non-passthrough name to the local
# https tier (architecture/reverse-proxies.md §5). Binding 443 here double-binds
# the port across http{} and stream{} — `nginx -t` cannot detect it, and the
# failure mode is nginx silently serving stale certificates on every later
# reload, which the 24h internal certs sharing this proxy would surface first.
#
# There is no upstream. The console is a static SPA that opens WebSockets
# straight from the browser to the chains' own RPC endpoints
# (wss://rpc1-mainnet.quantus.com and the testnets), so nothing here proxies
# chain traffic and nothing here needs to know a node address.
server {
listen 127.0.0.1:14443 ssl proxy_protocol;
http2 on;
server_name qapi.blackbeard.observer;
ssl_certificate /etc/letsencrypt/live/qapi.blackbeard.observer/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/qapi.blackbeard.observer/privkey.pem;
# TLS 1.2 stays enabled, unlike the .internal vhost: this is a public site
# and its audience is not a controlled fleet.
ssl_protocols TLSv1.2 TLSv1.3;
# The vhost believes it is serving 127.0.0.1:14443, so any redirect it
# generates itself carries that port and is unreachable from anywhere but
# this machine. Latent for a pure SPA, but the console ships real
# directories under the webroot — public/rpc_schemas/ — so a request for one
# without a trailing slash would 301 to https://…:14443/ without this
# (architecture/reverse-proxies.md §4).
absolute_redirect off;
root /var/www/qapi.blackbeard.observer;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Vite's hashed asset filenames are immutable.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# index.html must never be cached, or a deploy leaves browsers loading a
# bundle whose hashed filenames no longer exist.
location = /index.html {
add_header Cache-Control "no-cache";
}
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy same-origin always;
# Deliberately no Content-Security-Policy. The console's entire purpose is
# to connect to chain endpoints the user names — including a custom one they
# type in — and to load the polkadot-api wasm. A connect-src list would have
# to be reopened for every endpoint anyone ever adds, and a stale one fails
# as "the console will not connect" with the reason only in the browser
# console. If a CSP is added later it needs 'wasm-unsafe-eval' and a
# connect-src that does not enumerate hosts.
}