refactor(registry)!: MeshCoreManager implements ISourceManager — one unified source registry (#3962)#4004
Conversation
…e guards + config module (#3962 Ph2) - Add `implements ISourceManager` to MeshCoreManager with all required members: - `get sourceType(): 'meshcore'` discriminant getter - `configure(cfg)` stores pendingConfig so parameterless start() can connect - `start()` / `stop()` delegating to connect(pendingConfig) / disconnect() - `getStatus(sourceName?: string): SourceStatus` (widened from required arg + narrow return) - `getLocalNodeInfo(): null` (meshcore has no nodeNum; meshcore-specific code uses getLocalNode()) - `setSourceName(name)` for the rename-handler path (WP3b call-site) - `sourceName` stored in constructor (fallback to sourceId, mirrors meshtastic quirk) - New src/server/sourceManagerTypes.ts — isMeshCoreManager / isMeshtasticManager type guards (sourceType-based, no instanceof, no import cycles; canonical narrowing idiom for codebase) - New src/server/meshcoreConfig.ts — meshcoreConfigFromSource, virtualNodeConfigFromSource, MeshCoreSourceConfig, DEFAULT_VIRTUAL_NODE_PORT moved out of meshcoreRegistry.ts - New tests: sourceManagerTypes.test.ts (guard truth table over all 4 sourceTypes), meshcoreConfig.test.ts (ported from meshcoreRegistry.test.ts), and sourceManagerRegistry.meshcore.test.ts (registry lifecycle + manual-disconnect invariant) Verification: tsc clean; test typecheck 283 errors (at baseline); lint:ci OK; full suite 8261 pass / 0 fail / 607 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…gerRegistry (#3962 Ph2) MeshCore sources now use sourceManagerRegistry (the shared registry) instead of the parallel meshcoreManagerRegistry. Every lifecycle path — startup auto-connect, source create/enable/disable/delete, config-change restart, manual connect/disconnect — goes through the create-or-connect recipe on sourceManagerRegistry. Key behaviors preserved: - Manual /disconnect calls manager.disconnect() and KEEPS the manager registered (routes 404 without it); only removeManager removes from map - autoConnect=false at startup skips registration entirely - Reconnect with changed config: removeManager + fresh MeshCoreManager The 3 scheduler constructors (TelemetryPoller, RemoteTelemetryScheduler, RoomSyncScheduler) now receive sourceManagerRegistry and filter internally with getAllManagers().filter(isMeshCoreManager), replacing the old registry.list(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…pe (#3962 Ph2) All getAllManagers() loops that assumed meshtastic-only content now use type guards or explicit sourceType checks: - Security/dup-key/dead-node loops: `.filter(m => m.sourceType !== 'meshcore')` because MeshCore does not participate in PKI key dedup or Meshtastic dead-node detection. - Low-battery / inactive-node services: unified enumeration via getAllManagers() only (removed the old dual-registry merged array that appended meshcoreManagerRegistry.list() alongside sourceManagerRegistry). MeshCore managers now appear here naturally because WP2 registers them in sourceManagerRegistry. - Announce/timer/geofence/airtime loops in server.ts: filter(isMeshtasticManager) before calling meshtastic-only methods (restartAnnounceScheduler etc.). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
- meshcoreTelemetryPoller.test.ts / meshcoreRemoteTelemetryScheduler.test.ts: switch registry mock from MeshCoreManagerRegistry.list() to SourceManagerRegistry.getAllManagers(); add sourceType: 'meshcore' to fake manager objects so isMeshCoreManager() filter passes - sourceRoutes.autoConnect.test.ts / sourceRoutes.status.test.ts: add vi.mock stubs for meshcoreConfig.js and meshcoreManager.js (now imported by sourceRoutes.ts); update disconnect/status/nodeCount tests to use unified mockRegistry.getManager with sourceType: 'meshcore' instead of old mockMcRegistry.get - lowBatteryNotificationService.test.ts / inactiveNodeNotificationService.test.ts: MeshCore beforeEach now puts manager in mockGetAllManagers (unified) rather than the removed mockMeshcoreList; both test files still mock meshcoreRegistry (unused by production code after WP2 but harmless to keep for isolation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…Order) Restores the typecheck:tests baseline to 283. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…ly call sites to sourceManagerRegistry (WP3b) All production imports of meshcoreManagerRegistry / meshcoreRegistry have been removed from routes and services. Every call site now uses sourceManagerRegistry.getManager(id) narrowed by isMeshCoreManager() from sourceManagerTypes.ts, or sourceManagerRegistry.getAllManagers().filter(isMeshCoreManager) for list operations. Files changed: - routes/channelRoutes.ts: two .get() calls (PUT + DELETE channel for MeshCore) - routes/meshcoreRoutes.ts: import, managerFor(), router guard, two .list() calls (saved-region cache refresh), three .get() calls inside handlers simplified to managerFor(req) since the router guard already guarantees the manager - routes/messageRoutes.ts: .list().filter(isConnected) for message search - routes/sourceRoutes.ts: null-guard on isMeshCoreManager (pre-existing TS2345) - routes/unifiedRoutes.ts: .get() in unified stats; stale comment updated - routes/v1/messages.ts: .list().filter(isConnected) for v1 message search - services/automation/meshActionDeps.ts: drop dual-registry fallback in resolveManager() - services/automation/meshNodeData.ts: getSelfPublicKey via isMeshCoreManager narrow - services/sourceDashboardData.ts: buildSourceNodes MeshCore branch via isMeshCoreManager Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…erRegistry (WP3b) Production code no longer consults meshcoreManagerRegistry, so tests that stubbed it were exercising dead mocks: - meshcoreRoutes.test.ts: registry mock -> sourceManagerRegistry (partial mock); stub manager gains sourceType:'meshcore' for the isMeshCoreManager() guard - unifiedRoutes.test.ts: meshcore node-count test drives getManager(); mc manager mock gains sourceType + getStatus (anyConnected loop touches it now) - channelRoutes.test.ts: registry mock -> sourceManagerRegistry.getManager - meshActionDeps.test.ts: #3915 fallback regression tests rewritten for the single-registry resolution (#3962 Ph2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…3962 Ph2) - Reduce meshcoreRegistry.ts to a @deprecated shim delegating to sourceManagerRegistry + isMeshCoreManager; delete MeshCoreManagerRegistry class; re-export config helpers from meshcoreConfig.ts; getOrCreate throws a migration-guidance error. Remove annotation: one release then delete. - Replace meshcoreRegistry.test.ts lifecycle suite with shim-behavior tests: delegation semantics, @deprecated surface shape, getOrCreate throws, and meshcoreConfigFromSource re-export. Config-helper tests stay in meshcoreConfig.test.ts (moved in WP1). - Opportunistic cleanup: remove inert vi.mock('../meshcoreRegistry.js') blocks and stale mockMeshcoreList/mockMcRegistry variables from six sourceRoutes test files and two notification-service test files (production code no longer imports meshcoreRegistry after WP3b migration). - CLAUDE.md: update Multi-Source table (single unified registry); add critical rules for isMeshCoreManager/isMeshtasticManager guards and MeshCore disconnect semantics; note @deprecated shim for deletion. - REMEDIATION_EPIC.md: add Phase 2 checklist (2.1 this task, 2.2a–d heartbeat/announce/responder/distance-delete, 2.3 singleton retirement); record Phase 2 start date 2026-07-07. Verification: typecheck clean; typecheck:tests 282 errors (≤283 baseline); lint:ci exit 0; full suite success:true / 8258 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…esolveSourceManager - resolveSourceManager: check isMeshtasticManager before returning the registered manager; meshcore ids deliberately fall back to the singleton - messageRoutes: purge-node resolver uses isMeshtasticManager instead of raw cast - securityRoutes: add isMeshtasticManager guards on GET /dead-nodes, POST /dead-nodes/bulk-delete, POST /scanner/scan; convert plain json errors to fail() envelope with SOURCE_NOT_FOUND / INVALID_SOURCE_TYPE codes - New test: resolveSourceManager.test.ts covers meshtastic, meshcore, missing id - Existing securityRoutes.test.ts extended with INVALID_SOURCE_TYPE guard tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
Previously delegated to sourceManagerRegistry.stopAll() which stopped ALL managers (including meshtastic). Now filters to meshcore managers only before removing, leaving meshtastic sources untouched. Adds regression test verifying meshtastic manager survives disconnectAll(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…Name in start() logs - connect(): sets this.pendingConfig = config alongside this.config = config so the staging field stays in sync when connect() is called directly (not via start()) - start(): all three log lines now include (sourceName) alongside sourceId for easier multi-source log triage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…re-lookup The router-level guard now stores the narrowed MeshCoreManager in res.locals.meshcoreManager after the isMeshCoreManager check succeeds. managerFor() reads from res.locals instead of calling getManager() a second time, eliminating the TOCTOU window between guard check and handler use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
- server.ts: clarify debug log to say 'registered as meshcore and connected' - CLAUDE.md: replace 'one place sourceType===meshcore is branched on in route logic' with accurate description of why MeshCore disconnect semantics differ from meshtastic (disconnect keeps the manager registered rather than removing it) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…ail() envelope - channelRoutes: PUT /:id and DELETE /:id MeshCore 503 responses now use fail(res, 503, 'SOURCE_NOT_CONNECTED', ...) for consistent error shape - securityRoutes: SOURCE_NOT_FOUND and INVALID_SOURCE_TYPE already committed in fix(security) commit above; this commit covers the channelRoutes changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
…e-filter - meshcoreConfig.ts: add ensureMeshCoreManagerStarted(source, cfg) — the canonical create-or-connect recipe (create if missing, reconnect if disconnected, skip+log if already connected) - sourceRoutes.ts: replace 4 inline create-or-connect blocks with the helper; the /connect endpoint with alreadyRunning logic is kept at its call site - server.ts: startup auto-connect loop also replaced with the helper; removes the now-redundant MeshCoreManager import from server.ts - messageRoutes.ts + v1/messages.ts: collapse .filter(isMeshCoreManager) .filter(m => m.isConnected()) into a single type-predicate filter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
|
Claude finished @Yeraze's task —— View job PR Review: MeshCoreManager implements ISourceManager — unified registry (#3962)Todo List:
SummaryThis is a high-quality, well-executed structural refactor. The adversarial review process the PR describes is clearly visible in the result — regression tests exist for the specific TOCTOU and type-safety bugs that unification would have introduced. The new unified registry approach is architecturally sound. My comments below are mostly minor quality observations and a couple of things worth discussing. Observations1.
|
…race (#4007) PR #4002 (scheduled device-reboot action) was authored against the pre-#4004 dual-registry mocks (meshcoreGet) and merged after #4004 removed them — both PRs green individually, main red in combination (5 ReferenceError failures). Production code was unaffected (it duck-types rebootDevice via the unified resolveManager). Tests now use the unified-registry mock pattern like the rest of the file. Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Task 2.1 of the remediation plan (#3962, Phase 2) — the highest-leverage structural change of the phase: one registry, one source-manager interface, no parallel MeshCore world.
What changed
MeshCoreManager implements ISourceManager: newsourceTypegetter,configure()/parameterlessstart()/stop()lifecycle (config staged on the instance), widenedgetStatus(sourceName?),setSourceName()(kept fresh on rename),getLocalNodeInfo() → null(documented — MeshCore has no nodeNum).sourceManagerRegistry; all lifecycle (create/enable/disable/delete/config-change/connect/disconnect) and all ~30 read sites migrated. The load-bearing MeshCore quirk is preserved and now documented: manual/disconnectstops the manager but keeps it registered (routes 404 without it).isMeshCoreManager()/isMeshtasticManager()insourceManagerTypes.tsreplaceas (typeof meshtasticManager)[]casts; every meshtastic-assuming enumeration loop is hardened (security/dup-key/dead-node exclude meshcore; low-battery/inactive use the unified enumeration that replaces their explicit dual-registry merge).meshcoreRegistry.tsreduced to a@deprecatedshim (delete after one release):get/list/removedelegate to the unified registry,disconnectAllfaithfully scoped to meshcore-only; config helpers moved tomeshcoreConfig.ts. Zero production callers remain.ensureMeshCoreManagerStarted()helper replaces the create-or-connect recipe that had been copy-pasted at 5 call sites.Found and fixed by the adversarial review (8 finder angles, 11 verifiers)
Confirmed regression class — by-id lookups: pre-PR,
getManager(<meshcore-id>)returnedundefinedand several Meshtastic-only paths leaned on that as an implicit guard. Post-unification a truthy MeshCoreManager reachedsendTraceroute/sendRemoveNode/isNodeInDeviceDbcalls → TypeError 500s (one reachable via attacker-controlled request body). Fixed at the shared-resolver altitude withisMeshtasticManagerguards restoring exact pre-PR semantics:resolveSourceManager(covers v1 actions), purge-node, dead-nodes, bulk-delete, security scan — with regression tests.Also from review: shim
disconnectAllno longer tears down every source type;connect()keepspendingConfigin sync;meshcoreRoutesguard now caches the narrowed manager inres.locals(kills a TOCTOU + honors the type-guard rule); touched handlers converted to thefail()envelope; log/doc accuracy nits.Behavior notes
Behavior-preserving by design, verified per-site. Deliberate calls: VN status stays meshtastic-filtered; security-class scans keep excluding meshcore; graceful-shutdown semantics untouched (follow-up filed).
Verification
success: true). Typecheck clean; test-typecheck 282 (one below the 283 baseline);npm run lint:cigreen.Refs #3962
🤖 Generated with Claude Code
https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n