06 — Troubleshooting & versioning
Install problems, runtime limits, and how SDK versions relate to the server and the world-schema ladder.
Install
404 / E404 on @blueforge-studio/* — the scope must point at packr:
# ~/.npmrc
@blueforge-studio:registry=https://api.packr.blueforge.studio
Anonymous reads work; no token is required to install.
workspace:* or @world-map-editor/* in a consumer install — that
would mean a broken package: workspace dependencies are bundled into
dist, never published. Reinstall from packr; if you see it, the
published tarball is bad — check the publish.yml run for the version
that shipped.
Runtime
| Symptom | Cause / fix |
|---|---|
WebSocket is not defined from /realtime | Node < 22 has no global WebSocket. Pass webSocketImpl (require('ws').WebSocket) to openRealmProvider, or upgrade to Node ≥ 22. |
403 realm_requires_user_session | The realm needs a user JWT — API keys are refused by design. Use REST + SSE for document reads. |
403 read_only_role on a realm ticket | Players/observers don't get the shared document; they use client.worlds.get (the server's per-role read model) + the SSE stream. |
| Realm won't reconnect after idle | Reconnects fetch a fresh ticket automatically and, as implemented, keep retrying with backoff on any ticket failure (including a refusal) until destroyRealmProvider. If the session was revoked, destroy the provider, re-authenticate and call openRealmProvider again. |
WorldForgeApiError 403 missing: ['world:write'] | The key's scopes don't cover the route — see the authentication guide. |
commit_required 400 on worlds.patch | PATCH needs exactly one of confirm: true / propose: true. |
| SSE stream ends without error | The stream is edge-triggered with no replay and no auto-reconnect: re-open it and re-read GET /worlds/:id to catch up. |
McpToolError | The tool ran but rejected (e.g. scope, validation) — read .message / .result. Thrown by callTool and callToolRaw. Transport-level failures (401) are not McpToolError and, from this wrapper, not WorldForgeApiError either — they come from the MCP SDK. |
Invalid literal value, expected "<current>" on world.schemaVersion | You validated an older document. Run runMigrations first, then safeParseWorld (see 03). |
No migration from <v> to <current> | runMigrations has no rung for that version — the document is newer than this SDK (upgrade the SDK) or not a world. |
WorldForgeConfigError | createClient got no auth option. (More than one is not an error: apiKey wins over jwt over getToken.) |
Versions
Three version numbers move independently:
- SDK package version (this package, semver). Breaking = any
signature change to
createClientresources or the entrypoint surface; therawescape hatch is the stable backstop. WORLD_SCHEMA_VERSION— the world-document ladder (currently0.53.0at the time of this audit), exported from/offline. The SDK bundles the engine, so an SDK release can trail a server that has already moved the ladder.runMigrationsbrings an older document up; the server rejects documents it doesn't recognise, so migrate before upload. Acommit_required/invalid_bodyerror after a server schema bump usually means: upgrade the SDK (or run/offlinemigrations first).- The API itself — additive routes ship continuously (see
docs/api/, kept honest by a CI drift test). New routes appear before the SDK wraps them;client.rawreaches them on day one.
Release practice
No changesets in this repo: bump version in
packages/sdk/package.json in the PR that changes the SDK surface.
Publishing is automatic — publish.yml runs on main pushes touching
packages/sdk/**, builds, and skips versions already on packr
(so every main push is safe to re-run). To verify what's live:
curl -s https://api.packr.blueforge.studio/@blueforge-studio%2Fworldforge-sdk \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s)['dist-tags'].latest))"