Skip to content

v0.55.0 — Module API 1.24.0: the host tells modules where its API is

The durable half of the v0.54.1 hotfix, which stopped 5e feats and spells returning HTML in production.

Why a seam rather than the patch alone

v0.54.1 resolves relative /api/... paths inside apps/web's global fetch. That works, and it protects every module including ones nobody has touched — but it repairs the symptom at the boundary, after a module has already built a URL that cannot reach the API. A module holding the global fetch has no way to know that its own routes are on another host, and nothing it can observe locally will tell it: the Vite dev server proxies /api onto the one origin the page was loaded from, so the split simply does not exist until deploy.

The registry already has the answer-shaped hole next door. setAuthTokenProvider exists because Clerk lives in the host and a module cannot mint a token. The origin is the same kind of fact:

moduleRegistry.setApiBaseUrl(baseUrl)  // host, once at startup
moduleRegistry.resolveApiUrl(path)     // module, per request

Both delegate to resolveApiUrl in packages/shared/src/utils/apiUrl.ts, which v0.54.1 introduced and which is now the single implementation of the rule. useAuthFetch's own copy is gone — three call sites agreeing by construction beats three regexes kept in step, and two of them disagreeing about what counts as an API path is exactly how a module ends up parsing HTML as JSON.

Additive, hence a minor bump. A module that keeps concatenating relative paths behaves as before, and a module that adopts resolveApiUrl still runs on an older platform, where the call degrades to the relative path rather than throwing. dnd5e 0.7.1 therefore keeps its ^1.5.0 engine range.

The guard runs where CI runs

moduleApiOrigin.guard.test.ts lives in server-core rather than beside the code it checks. apps/web's test script is a stub, so CI never runs that suite, and a guard against a production-only regression has to sit where CI executes — the same reasoning as ciPackagesAuth.test.ts. It pins the registry seam, the host wiring in App.tsx, the surviving global resolution in index.tsx, and that all three resolve through the one shared implementation. Every assertion was checked against a deliberately broken copy.

Hosted modules

modules.hosted.json moves @purple-cat-games/dnd5e to 0.7.1, which routes its SRD content fetches through the new seam.

Publish before deploying. dnd5e/v0.7.1 must be pushed and published to GitHub Packages before this release is deployed, or the frontend module install fails against a version that does not exist yet.

dnd5e 0.7.1 was branched from the dnd5e/v0.7.0 tag, not from that repo's main. The two have diverged: the tag is 24e62f5, main is ae638e7, and main descends from the dnd5e/v0.6.1 commit whose adaptations to an older platform the 0.7.0 notes state were not carried forward — on that line they were. Against this platform, vtt-modules main fails tsc with 6 errors (character.origins where character.origin is meant in validateOrigin and validateSkills, the rest prompts' kind widened to boolean/choice, a CharacterPower cast) and 4 tests in validateSkills.test.ts fail; the tag builds and tests clean. Publishing from main would have shipped a regression. Reconciling the two lines is its own job and is not done here.

Verified

pnpm exec eslint packages --quiet exits 0; pnpm test green (shared 44 files, server-core 132, ui-components 25, client-sdk 6, setup 3). apps/web tsc baseline unchanged at 203. Module side: tsc clean and 864 tests across 44 files, including 5 new ones that stand in for the environment that cannot be reproduced locally by asserting the URL the provider would actually request.