diff --git a/backend/routes/pods.ts b/backend/routes/pods.ts index 3d3c4621..f53134d0 100644 --- a/backend/routes/pods.ts +++ b/backend/routes/pods.ts @@ -426,8 +426,27 @@ router.get('/:podId/external-links', auth, async (req: AuthReq, res: Res) => { router.get('/:id/children', auth, async (req: AuthReq, res: Res) => { try { - const children = await Pod.find({ parentPod: req.params?.id }).populate('createdBy', 'username profilePicture').populate('members', 'username profilePicture').sort({ name: 1 }); - return res.json(children); + const parentId = req.params?.id || ''; + // Children listing reveals pod existence + names + members under a + // parent. Gate by the parent's visibility so a non-member can't + // enumerate someone else's nested workspace structure. The full + // results array is then filtered to children the caller can also + // see — matches the principle from PR #375 (no leak via fan-out + // queries either). + const parent = await Pod.findById(parentId).select('_id members type').lean(); + if (!parent) return res.status(404).json({ msg: 'Pod not found' }); + const canViewParent = await DMService.canViewPod(req.user?.id, parent); + if (!canViewParent) return res.status(403).json({ msg: 'Not authorized to view child pods' }); + const children = await Pod.find({ parentPod: parentId }) + .populate('createdBy', 'username profilePicture') + .populate('members', 'username profilePicture') + .sort({ name: 1 }); + const visibleChildren = []; + for (const child of children) { + // eslint-disable-next-line no-await-in-loop + if (await DMService.canViewPod(req.user?.id, child)) visibleChildren.push(child); + } + return res.json(visibleChildren); } catch (err) { console.error('Error fetching child pods:', (err as Error).message); return res.status(500).json({ msg: 'Server error' }); diff --git a/backend/routes/summaries.ts b/backend/routes/summaries.ts index 2423f919..7b7cf1e9 100644 --- a/backend/routes/summaries.ts +++ b/backend/routes/summaries.ts @@ -18,6 +18,10 @@ const { AgentInstallation } = require('../models/AgentRegistry'); const dailyDigestService = require('../services/dailyDigestService'); // eslint-disable-next-line global-require const auth = require('../middleware/auth'); +// eslint-disable-next-line global-require +const Pod = require('../models/Pod'); +// eslint-disable-next-line global-require +const DMService = require('../services/dmService'); interface AuthReq { user?: { id: string }; @@ -177,6 +181,15 @@ router.get('/all-posts', auth, async (_req: AuthReq, res: Res) => { router.get('/pod/:podId', auth, async (req: AuthReq, res: Res) => { try { const { podId } = req.params || {}; + // Pod-scoped summaries inherit pod visibility — non-members of personal + // pods (agent-room / agent-dm / agent-admin) must not learn pod + // existence or its summarized content. Parity with the /announcements + // + /files + /external-links gates on the pods router. + const pod = await Pod.findById(podId).select('_id members type').lean(); + if (!pod) return res.status(404).json({ error: 'Pod not found' }); + const userId = req.userId || req.user?.id; + const canView = await DMService.canViewPod(userId, pod); + if (!canView) return res.status(403).json({ error: 'Not authorized to view this pod summary' }); const podSummary = await ChatSummarizerService.getLatestPodSummary(podId); res.json(podSummary); } catch (error) {