Uh oh!
There was an error while loading. Please reload this page.
fix(cloud-connection): derive features.marketplace from what is actually mounted (#8356) - #8387
Conversation
…ed (#8356) RuntimeConfigPlugin built its /api/v1/runtime/config response with `marketplace: true` as a literal, so every runtime mounting the plugin told the Console the catalog was browsable -- including runtimes where MarketplaceProxyPlugin was never mounted because no control plane resolved. Same declared-is-not-enforced shape as #8343, one key over. The flag is now read per request off the route table of the app serving the response. Measured rather than assumed: the proxy registers no service (its init says so), and IHttpServer.getMountedRoutes/resolveMountedRoute exclude framework-native getRawApp() mounts by construction -- so the raw app's own route ledger is the only honest seam, and it is also the one that covers the cloud control plane, which serves the catalog natively with no proxy. install-local routes are excluded from the derivation: they are the offline half, mounted exactly on the runtimes with no catalog. Both directions pinned, with the positive case mounting the REAL proxy on the shared app rather than copying its route string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8356
RuntimeConfigPluginbuilt its/api/v1/runtime/configresponse withmarketplace: trueas a literal, so every runtime that mounted the plugin told theConsole the package catalog was browsable — including runtimes where
MarketplaceProxyPluginwas never mounted because no control plane resolved. The SPArendered a browse affordance the runtime could not serve. Same declared-is-not-enforced
shape as #8343, one key over.
The flag is now observed per request, off the route table of the app serving the
response.
The seam — measured, not inherited
The card proposed that the proxy "registers a service or is discoverable on the kernel at
kernel:ready". It does not, and that mattered enough to change the design:MarketplaceProxyPluginregisters no service. Itsinitsays so in as many words("No services registered — pure HTTP wiring during
start()"). It announces itselfonly by mounting
/api/v1/marketplace/*on the raw app. So there was nothing on thekernel to look up — and adding a registration purely to read it back would have been a
mechanism invented for its own observation rather than a fix.
IHttpServermount-introspection members cannot see it either.getMountedRoutes()andresolveMountedRoute()look like the right contract, but bothare scoped to routes registered through the adapter's own verb methods — "routes an
adapter mounts on its framework-native handle behind
getRawAppare outside this tableby construction" (the contract's own words in
packages/spec/src/contracts/http-server.ts),and
resolveMountedRoutefilters the live router's verdict back through that sameledger. The proxy mounts through
getRawApp(), so the adapter ledger reports nothingfor it.
Cloud control plane serves
/api/v1/marketplace/packages*natively, from its ownroute module, with no proxy anywhere. Its own declaration of this flag already reads
"
/api/v1/marketplace/*is reachable (proxy or native)". Keying on the proxy wouldreport
falseon the one deployment that definitely has a catalog.The raw app's route ledger is the union of everything registered on it — adapter verb
methods and framework-native mounts alike — so it answers exactly the question the flag
claims to answer, for every distribution, with no new registration invented. Measured
against hono@4.12.34:
app.routescollects verb methods,all()anduse()alike.Read per request rather than snapshotted at
kernel:ready: pluginstart()orderacross hooks is not guaranteed, and by request time every hook has run. There is a test
for exactly that (a proxy mounted after this plugin still counts).
/api/v1/marketplace/install-localis excluded from the derivation on purpose: it is theoffline install half, mounted precisely on the runtimes that have no catalog, and counting
it as browse would recreate this defect one key over.
No config knob
Per the #8343 ACCEPT ruling of 2026-08-13, and for the reason the card gives: a
marketplace?: booleanoption repeats one layer up the every-host-must-remember failurethat propagated the original defect into the self-hosted EE image, where both the host
config and this package's README kept a hand-maintained flag out of step with their own
mounting.
Both directions pinned, and the negative direction ablated
packages/cloud-connection/src/runtime-config-marketplace-derivation.test.ts— thepositive cases mount the real
MarketplaceProxyPlugin/MarketplaceInstallLocalPluginon the shared app rather than copying their route strings, so a change to the proxy's
prefix fails the suite instead of silently flipping this flag to
falsein production.Ablation (predicted before running: negative cases red, positive cases green) — reverting
the derivation to the pre-fix literal
marketplace: true:The fix was committed before the ablation and restored from that commit;
git hash-objectconfirms the restored file is byte-identical to the pre-ablationstate (
8b878abef715e1966612083b7f1ed8f6769ea075).Behaviour change
A runtime that mounts a marketplace browse surface reports exactly what it did before. A
runtime that mounts none now reports
marketplace: falseinstead oftrue— thecorrection — and its Console stops offering catalog browse it cannot serve. A cloud-less
runtime can therefore report
installLocal: truetruthfully without also claimingbrowse, which is the constraint that kept #8343 from mounting this plugin on an air-gapped
runtime at all.
Escape hatch unchanged: the derivation is the base value, not a veto. The open-core
resolveFeaturesseam still merges over it, so a host on an adapter whose raw app exposesno route ledger — where the flag conservatively reports
false, with a warning logged onceat mount time — can still declare the capability it knows it serves.
Verification
pnpm --filter @objectstack/cloud-connection test— 18 files, 123 tests, all passing.pnpm --filter '@objectstack/cloud-connection^...' build— dependency closure builtfirst (30 projects), so the run is not reading stale
dist.features.marketplaceother than the plugin itself; the SPA consumer lives in
objectuiand reads it as theboolean it already was.
check:nul-bytes,check:changeset-gate-self-tests,check:objectui-changeset,check-changeset-no-major,check:query-options-erasure,check:type-check-coverageall green.
check:objectui-pin-freshis red on a stale.objectui-sha— a repo-widerelease-time gate on a file this diff does not touch (
git diff origin/main -- .objectui-shais empty).
Backlink: #8343. Related: #8355 (the README example, sequenced behind this) — that card
gets simpler as a result: its trap 2 ("an example that mounts
RuntimeConfigPluginoutside the cloud branch advertises browse as well") is retired here, so the corrected
example can mount
RuntimeConfigPluginunconditionally, which is what an air-gappedruntime needs.
Generated by Claude Code