Summary
The environment connection model (about 1.9k lines that were already DOM-free) lived in apps/web/src/lib, where nothing outside the web app could import it. It now lives in packages/environment-client/src/connection/ and is exported as @openmanager/environment-client/connection, so the React Native app can use the same code.
What moved (with git mv, so history follows)
bootstrap, pairing, route-fallback, route-health, connection-state, environment-store and their tests. The tests now live in packages/environment-client/tests/connection/ (the package's test home) and run in a node environment, which also shows they need no DOM. The subpath is a source-only exports entry declared exactly like the package root (types + default pointing at ./src/connection/index.ts). Web resolves it the same way it already resolves the root.
Seams
- Registry storage.
readEnvironmentRegistry,writeEnvironmentRegistryandclearEnvironmentRegistrytake aRegistryStorage(getItem,setItem,removeItem). They used to fall back towindow.localStoragethemselves. Web now passesbrowserRegistryStorage(apps/web/src/lib/registry-storage.ts), which looks upwindow.localStorageon each call. It does this because readingwindow.localStorageitself throws where storage is blocked, and that throw has to happen inside the registry functions, which already read it as an empty or unsaved registry. - Loopback permission.
fetchBootstrap,probeRouteHealthandsearchRoutestake aloopbackAccessDeniedcheck. It is only asked when a fetch to this device's loopback address fails, so a refused browser permission shows as "blocked" rather than "nothing listening". The check is required, not defaulted. If it were optional, a caller that forgot it would quietly lose the "blocked" answer, and the types would not complain. A host with no such gate passesnoLoopbackGateexplicitly.searchRoutesonly asks for the check when no customprobeis supplied. Web passes its existingloopbackAccessDeniedat every call site.
What stayed browser-only, and why
pairingAppUrlreadswindow.locationandimport.meta.env.BASE_URL, which only make sense for the web app. It is now inapps/web/src/lib/pairing-app-url.ts.loopbackAccessDeniedreadsnavigator.permissions. It stays inapps/web/src/lib/local-access.tsand is injected as described above.local-owner.ts,environment-cache.ts,browser-runtime.tsandproviders/connection-provider.tsxstay inapps/webunchanged, apart from import paths and passing the two seams.
Keeping the folder host-neutral
The root eslint.config.mjs adds no-restricted-globals for window, document, localStorage, navigator, indexedDB and location on packages/environment-client/src/connection/**. It also adds no-restricted-properties for the globalThis. / self. forms of the same names. I checked the rule by planting a file that uses each one: lint reported all six.
Web behaviour unchanged
- No protocol change. Error messages, timeouts, registry versioning and the legacy-key migration are the same.
- The story-fixture checks that ran
deriveConnectionUiover the web stories now sit next to those stories (apps/web/src/stories/connection-states.test.ts), so the package does not depend on web fixtures. environment-client-provider.test.tsxstill drives a realloopback-network: deniedpermission throughConnectionProvidertolocal_access_blocked. That covers the injected check end to end.- New tests cover the storage seam on web (
registry-storage.test.ts, including a throwinglocalStoragegetter) and the loopback seam in the package (the check is asked for loopback routes and never for remote ones, forfetchBootstrap,probeRouteHealthandsearchRoutes).
Verification
pnpm --filter @openmanager/environment-client typecheck,lint,test: 16 files, 374 tests passpnpm --filter @openmanager/web typecheck,lint: clean- Web vitest on Node 24.14.1: 25 files, 236 tests pass
- Prettier is clean on every file I touched. Four files were already unformatted on main (the three docs and
connection-state.ts, which moved unchanged), and I left those as they were.
🤖 Generated with Claude Code
Activity
6🚀 Deploying Preview to Cloudflare 🚀
Preview Deployments by commit
| Status | Deployment URL | Commit | Updated (UTC) | See this deployment's details |
|---|---|---|---|---|
View logs ↗ | 09761b0 | 2026-10-20T17:19:38.195Z | View logs ↗ | |
View logs ↗ | 1df8b59 | 2026-10-20T16:33:10.687Z | View logs ↗ |
[Medium impact] The PR appears safe to merge; the documentation update matches the storage code.
Summary
Moves the shared connection code into
@openmanager/environment-client/connection.Diagram
Reviews (2) · Last reviewed commit: "Describe web's registry storage adapter ..." · Reviewed by Greptile