Conversation
|
|
||
| router.get('/', auth, getAllPods); | ||
| router.post('/', auth, createPod); | ||
| router.patch('/:id', auth, updatePod); |
|
|
||
| router.get('/', auth, getAllPods); | ||
| router.post('/', auth, createPod); | ||
| router.patch('/:id', auth, updatePod); |
| || slugify(`${item.agentName}-${item.instanceId}`) === normalizedLabel | ||
| )) || null; | ||
| } | ||
|
|
| @@ -153,6 +263,10 @@ router.post('/:podId', auth, async (req: AuthReq, res: Res) => { | |||
| } | |||
| } | |||
samxu01
left a comment
There was a problem hiding this comment.
Big PR, well-scoped feature. The data model for project + task is reasonable and the room/tab split into PodRoomRouter is a clean extension point. Main concerns are one real security bug and a few data-loss / consistency issues worth fixing before merge.
Please fix before merge:
- Stored XSS via
keyLinks[].url(ProjectPodRoom.tsx:750,podController.ts updatePod).javascript:URIs render into<a href>. Needs scheme validation server-side and guard on render, plusnoopener. - Complete clobbers blocker history (
tasksApi.ts:313). Only flipopen/ stampresolvedAt; preservereason/openedAt/openedBy.
Consistency / tech debt:
updatePodignoresprojectMeta.ownerIds(podController.ts:336). Schema advertises multi-owner, auth check only honorscreatedBy. Either wireownerIdsinto the auth check or drop it from the schema.// @ts-nocheckon 1108-lineProjectPodRoom. Minimum: typeroom,task,podAgentsfrompackages/types.projectMetastored on every pod (Pod.ts:75). Schema defaults populate the subdoc on chat/study/games/dm too. Gate ontype === 'project'.
Not posting inline but worth tracking:
- Optimistic-message coalescing in the socket handler matches on
contentonly — two identical messages in quick succession collapse. Pre-existing pattern elsewhere, not new here. PATCH /:podId/:taskIdacceptsblockeras an opaque object in theallowedlist. Mongoose enum validation will reject invalidseverity, but nothing prevents callers from flippingopenedAt/openedBydirectly. Consider a dedicated/blockerendpoint that stamps those server-side.frontend/start-dev.shusesnpm ci --only=production=false— the--onlyflag was deprecated in npm 7+ and this syntax is malformed.npm ciinstalls devDeps by default; just drop the flag.enqueueTaskAssignedIfNeededfallback lookup does afind({podId, status:'active'})+ in-memory slug match. Fine at current pod sizes; worth an index check if pods grow.PodRoomRouter.tsx:14— 14-line router is a good abstraction; future work should keep this stable when adding more pod types.
Marking as COMMENT; the XSS and blocker-clobber are real defects but contained.
Generated by Claude Code
| {(room?.projectMeta?.keyLinks || []).length ? ( | ||
| room.projectMeta.keyLinks.map((link, index) => ( | ||
| <a key={`${link.url}-${index}`} href={link.url} target="_blank" rel="noreferrer" style={{ color: '#f8fafc', textDecoration: 'none' }}> | ||
| {link.label || link.url} |
There was a problem hiding this comment.
Stored XSS. link.url is rendered into href= with zero scheme validation, and the backend updatePod stores the string as-is (only trims whitespace). Any pod editor can save javascript:alert(1) — every member who clicks gets XSS.
Fix: validate scheme server-side (allow http:/https:/mailto: only) and guard in the render:
const safeUrl = /^(https?|mailto):/i.test(link.url) ? link.url : '#';
<a href={safeUrl} target="_blank" rel="noopener noreferrer">...Also add noopener — noreferrer alone doesn't cover older browsers for target="_blank".
Generated by Claude Code
| $set: { | ||
| status: 'done', | ||
| completedAt: new Date(), | ||
| progressPercent: 100, |
There was a problem hiding this comment.
Completing a blocked task $sets the entire blocker object to a fresh default — reason: null, waitingOn: null, openedAt: null, openedBy: null. That erases the audit trail of why/when the task was blocked.
Just flip open and stamp resolvedAt:
'blocker.open': false,
'blocker.resolvedAt': new Date(),Keep reason, waitingOn, openedAt, openedBy for history.
Generated by Claude Code
| } | ||
|
|
||
| const requesterId = req.userId || req.user?.id || req.user?._id; | ||
| const isCreator = pod.createdBy.toString() === requesterId?.toString(); |
There was a problem hiding this comment.
The schema introduces projectMeta.ownerIds (plural) but the auth check here only honors the original createdBy. A co-owner added via ownerIds gets 403 when they try to edit project metadata — they paid for the field but can't use it.
Either:
const isOwner = (pod.projectMeta?.ownerIds || []).some(
(id) => id.toString() === requesterId?.toString()
);
if (!isCreator && !isOwner && !isGlobalAdmin) { ... }or drop ownerIds from the schema until co-ownership is actually wired.
Generated by Claude Code
| @@ -0,0 +1,1108 @@ | |||
| // @ts-nocheck | |||
There was a problem hiding this comment.
// @ts-nocheck on a 1108-line component that's the primary surface for a new feature is expensive tech debt from day one. The rest of the codebase is typed; this file will silently accept shape changes to room.projectMeta, task.blocker, task.assigneeType, etc., and the first signal of a break will be runtime.
Minimum ask: drop @ts-nocheck and at least type the props + the three main state shapes (room, task, podAgents) from packages/types/src/pod.ts. The any-tolerant handlers can stay loose for now; the data surface is what actually needs coverage.
Generated by Claude Code
| projectMeta: { | ||
| goal: { type: String, trim: true, default: '' }, | ||
| scope: { type: String, trim: true, default: '' }, | ||
| successCriteria: { type: [String], default: [] }, |
There was a problem hiding this comment.
projectMeta has field-level defaults but no conditional on type === 'project', so every pod (chat/study/games/dm/…) gets a populated projectMeta: { goal:'', scope:'', successCriteria:[], status:'planning', dueDate:null, ownerIds:[], keyLinks:[] } persisted.
Two small wins: save storage on non-project pods, and make the "is this a project?" check a simple field-presence test. Either mark the subdoc default: undefined and set it explicitly in the project branch of createPod, or gate with required: function() { return this.type === 'project'; }.
Generated by Claude Code
This PR introduces a first-pass project pod workspace.
What is included:
projectpod type in backend and shared typesChatandTaskstabsprojectMeta)Still pending:
todo,in_progress,blocked,in_review,doneVerification:
Pod.test.tsxafter the dropdown fix