fix(api): v1 'default' source alias deep-scopes by the resolved id, not the literal#4191
Conversation
…ot the literal The v1 API mounts canonical per-source routes at /api/v1/sources/:sourceId/... and supports a `default` alias. `attachSource` resolves the alias and normalises `req.params.sourceId` to the concrete resolved id. But Express RE-DERIVES `req.params` for each `mergeParams` sub-router from the matched URL, so handlers inside the sub-routers that read `req.params.sourceId` saw the raw URL literal "default", NOT the resolved id. Consequence: route resolution for `default` returned 200, but any handler that passed `req.params.sourceId` into a DB query deep-scoped by the literal "default" — matching no rows when the real default source id differs — silently returning empty results. Fix: add `resolvedSourceIdFromPath(req)` to sourceParam.ts, which prefers the request-level `req.source.id` (attached by `attachSource`, survives the re-derivation) and falls back to the raw path param. Route every per-router `getScopedSourceId` helper and the status/actions handlers through it. The existing `?sourceId=` query fallbacks (legacy root mounts) are left intact. Verified red→green with a new regression test that reproduces the exact production wiring (attachSource + a nested mergeParams sub-router) and asserts the nodes query is scoped by the resolved concrete id, returning the seeded rows, instead of the empty result the literal "default" produced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018e4dtLyWeYJYJ7SbgFvGG1
|
Claude finished @Yeraze's task —— View job PR Review: fix(api): v1 'default' source alias deep-scopes by the resolved idTasks:
SummaryThis is a well-targeted, correctly-reasoned bug fix. The root cause diagnosis is accurate (Express re-derives Code Quality ✅
The implementation is clean and correctly typed. export function resolvedSourceIdFromPath(req: Request): string | undefined {
const fromSource = (req as MaybeRequestWithSource).source?.id;
if (typeof fromSource === 'string' && fromSource) return fromSource;
return typeof req.params.sourceId === 'string' ? req.params.sourceId : undefined;
}The fallback to Router files — The
Potential Issues / Minor Observations1. const node = await databaseService.nodes.getNode(Number(localNodeNum));This call doesn't pass 2. Test — app.use(
'/api/v1/sources/:sourceId/nodes',
attachSource('nodes', 'read'),
positionHistoryRouter,
nodesRouter
);The comment says "Exact production wiring from index.ts" — this should be verified to actually match production mount order. If it doesn't, the test is still valid for the regression it covers, but the comment would be misleading. 3. Test — no non-admin user path covered The test only exercises the admin path ( 4. Test — The Security ✅No new security concerns. The fix narrows data exposure (queries now correctly scope by the real source id rather than a string that matches nothing), which is strictly an improvement. Performance ✅No performance regression. OverallThis is a correct, minimal, well-tested fix for a real data-exposure bug (empty results when using the Approve with one non-blocking suggestion: consider adding |
|
Re the non-blocking note: agreed |
Root cause
The v1 API mounts canonical per-source routes at
/api/v1/sources/:sourceId/...and supports adefaultalias that resolves to the primary/default source. TheattachSourcemiddleware (src/server/routes/v1/sourceParam.ts) resolves the alias, attaches the resolvedSourcetoreq.source, and normalisesreq.params.sourceIdto the concrete resolved id.The bug: Express re-derives
req.paramsfor eachmergeParamssub-router from the matched URL. So handlers inside the sub-routers (nodesRouter,messagesRouter, etc. — all mounted as nestedRouter({ mergeParams: true })) that readreq.params.sourceIdsaw the raw URL literal"default", NOT the idattachSourcewrote.req.source(a request-level property) survives the re-derivation and carries the resolved source.Consequence: requesting
/api/v1/sources/default/nodesreturned200, but the handler deep-scoped its DB query by the literal string"default", which matches no rows when the actual default source id differs — silently returning empty results.Verified empirically with a minimal Express repro: inside a nested
mergeParamssub-router,req.params.sourceId === "default"whilereq.source.id === "<concrete>".Fix
Added
resolvedSourceIdFromPath(req)tosourceParam.ts. It prefersreq.source?.id(survives re-derivation), falls back to the raw path param, and returnsundefinedon the legacy root mounts. Routed every per-routergetScopedSourceIdhelper plus thestatusandactionshandlers through it.The existing
?sourceId=query fallback branches (which serve the legacy root mounts) are left fully intact — only the path-derivation line changed. No newany(a narrow typedMaybeRequestWithSourceaccessor is used).Files touched:
sourceParam.ts— newresolvedSourceIdFromPathhelper +MaybeRequestWithSourcetypenodes.ts,channels.ts,messages.ts,network.ts,packets.ts,telemetry.ts,traceroutes.ts,positionHistory.ts—getScopedSourceIdnow derives the path id via the helperstatus.ts— inline path-scope now via the helperactions.ts— 4 handlers now read the resolved id via the helper (wasreq.params.sourceId as string)sourceParam.defaultAlias.test.ts— new regression testRed → green proof
New test
sourceParam.defaultAlias.test.tsreproduces the exact production wiring (attachSource+ nestedmergeParamssub-router) and seeds a source whose concrete id (src-primary-0001) differs from the literal"default", withnodes.getAllNodesreturning rows only for the concrete id.scopes the nodes query by the resolved concrete id→ failed (responsedataempty; query scoped by"default").getAllNodescalled with the concrete id and never with"default".Verification
success: true).tsc -p tsconfig.server.json --noEmit: clean.npm run lint:ci: exit 0 (no baseline change).Note for reviewers
The held PR #4189 removes the
?sourceId=query fallbacks (legacy root mounts) from these samegetScopedSourceIdhelpers. This PR only changes the path-derivation line and adds an import, so #4189 should need a trivial rebase.🤖 Generated with Claude Code