fix(shard): stop an undecryptable uo-link token 500ing every live-shard route #107
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/uolink-client-throw-and-sitemode-gate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Two fixes found by a live smoke test of the site — all 200 routes in
routes.manifest.jsonexercised at every access level (anonymous / player / moderator / editor / admin), plus a browser pass over the public site, player portal and admin panel.1. An undecryptable uo-link token 500'd every live-shard route
uoLinkClient.call()resolved the uo-link config outside itstry/catch.resolveConfig()decrypts the stored auth token, andsecretBox.decryptthrows when the ciphertext can't be authenticated —SECRET_ENC_KEYrotated, or a DB dump restored into an environment keyed differently. That throw escaped the client entirely, breaking its documented "never throws / always returns{ ok, data, status }" contract (module header,ARCHITECTURE.md, CLAUDE.md) and turning a misconfiguration into a hard 500 on every route that does a live sidecar round-trip:GET /admin/uo-link/configGET /{admin,player}/shard/char/:serialGET /{admin,player}/shard/roster/:accountGET /{admin,player}/shard/vendors/:accountPublic shard routes were unaffected — they read the DB via
getSafe(), which never decrypts.The fix moves
resolveConfig()inside thetryand logs the config-resolution failure atERRORwith a distinct message. Previously a wrong key looked identical to "the shard is offline", with nothing pointing at the real cause. NoteGET /admin/uo-link/configreturning 200 again matters for recovery: it is the screen an admin needs to re-enter the token, and having it 500 locked them out of the fix.2. The admin Dashboard offered editors a control they can't use
PUT /admin/site-modeisadminOnly, but the site-mode toggle rendered for every staff role, andtoggle()had atry/finallywith nocatch. An editor clicking it got an unhandled promise rejection (ApiError: Forbiddenin the console) and zero UI feedback — the button just said "Saving…" and reverted.Server-side enforcement was always correct, so this is UX, not a security hole. Gated on
role === 'admin'— the ruleAdminLayoutalready documents ("never show a non-admin a link that would 403") — and the rejection is now caught and surfaced.Docs PR: RunicGateway/docs#(fix/uolink-client-contract-and-sitemode-gate)
How it was tested
503with the sidecar still unreachable — the only variable was whether decrypt threw.server/test/uoLinkClient.test.js— verified it fails against the unfixed client (2 of 3 cases) and passes after.npm test(server): 437 pass / 0 fail.npm test(client): 43 pass / 0 fail.npm run build: clean.auth_token_encin the DB to reproduce the original condition, restarted the server, confirmed the four routes now return 200/503 with zero stack traces in the log and the newuo-link config unreadableline present.editor— the site-mode toggle is gone and the panel is read-only; signed in asadmin— the toggle is still there.Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
`uoLinkClient.call()` resolved the uo-link config OUTSIDE its try/catch. resolveConfig() decrypts the stored auth token, and secretBox.decrypt throws when the ciphertext can't be authenticated — SECRET_ENC_KEY rotated, or a DB dump restored into an environment keyed differently. That throw escaped the client entirely, breaking its documented "never throws / always returns { ok, data, status }" contract and turning a misconfiguration into a 500 on every route that does a live sidecar round-trip: GET /admin/uo-link/config GET /{admin,player}/shard/char/:serial GET /{admin,player}/shard/roster/:account GET /{admin,player}/shard/vendors/:account Found by a live smoke test of all 200 routes at every access level. Public shard routes were unaffected because they read the DB via getSafe(), which never decrypts. Move resolveConfig() inside the try so the failure returns the standard { ok: false } shape, and log it at ERROR with a distinct message: a wrong key previously looked identical to "the shard is offline", with no clue why. Those routes now degrade to 503, and GET /admin/uo-link/config returns 200 again — it is the screen an admin needs to re-enter the token and recover, so having it 500 locked them out of the fix. Also gate the admin Dashboard's site-mode toggle. PUT /admin/site-mode is adminOnly, but the button rendered for every staff role, and toggle() had a try/finally with no catch — so an editor clicking it got an unhandled promise rejection and zero UI feedback. Gate the control on role === 'admin' (the rule AdminLayout already documents: never show a non-admin a control that would 403) and surface a message if the call is refused anyway. Adds server/test/uoLinkClient.test.js, which fails against the unfixed client. Co-Authored-By: Claude <noreply@anthropic.com>