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.1must 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.