{"record":{"id":"6125ca6f71e5594a","repo":"thedotmack/claude-mem","slug":"project-id-must-belong-to-team-id","errorCode":null,"errorMessage":"project_id must belong to team_id","messagePattern":"project_id must belong to team_id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/storage/postgres/utils.ts","lineNumber":62,"sourceCode":"  text: string,\n  values: unknown[] = []\n): Promise<T | null> {\n  const result = await client.query<T>(text, values);\n  return result.rows[0] ?? null;\n}\n\nexport async function assertProjectOwnership(\n  client: PostgresQueryable,\n  projectId: string,\n  teamId: string\n): Promise<void> {\n  const row = await queryOne<{ id: string }>(\n    client,\n    'SELECT id FROM projects WHERE id = $1 AND team_id = $2',\n    [projectId, teamId]\n  );\n  if (!row) {\n    throw new Error('project_id must belong to team_id');\n  }\n}\n\nexport async function assertSessionOwnership(\n  client: PostgresQueryable,\n  serverSessionId: string,\n  projectId: string,\n  teamId: string\n): Promise<void> {\n  const row = await queryOne<{ id: string }>(\n    client,\n    'SELECT id FROM server_sessions WHERE id = $1 AND project_id = $2 AND team_id = $3',\n    [serverSessionId, projectId, teamId]\n  );\n  if (!row) {\n    throw new Error('server_session_id must belong to project_id and team_id');\n  }\n}","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/utils.ts#L44-L80","documentation":"assertProjectOwnership verifies that a given project ID actually belongs to the given team before allowing creates or API-key/audit-log writes. It runs a SELECT against the projects table filtering on both id and team_id; if no row matches, the relationship is invalid and the operation is refused. This prevents cross-team data injection through a valid but mis-scoped project_id.","triggerScenarios":"Calling create, createApiKey, createAuditLog, or validateSource with a projectId that exists but is owned by a different team, or with a projectId that does not exist at all.","commonSituations":"Copy-pasting a project UUID from another team's dashboard; stale cached project IDs after a project was moved or deleted; multi-tenant setups where environment variables for project and team come from different sources.","solutions":["Verify the projectId matches a row in the projects table for the exact teamId being used","Re-fetch the project list for the target team and use one of those IDs","Check environment/config files for values taken from a different team's account","Confirm the project was not moved or deleted; recreate it under the correct team if needed"],"exampleFix":"// before\nawait create({ teamId: 'team_a', projectId: 'proj_of_team_b' });\n// after\nconst projects = await listProjects('team_a');\nawait create({ teamId: 'team_a', projectId: projects[0].id });","handlingStrategy":"validation","validationCode":"const row = await queryOne('SELECT id FROM projects WHERE id = $1 AND team_id = $2', [projectId, teamId]);\nif (!row) throw new Error('project does not belong to team');","typeGuard":null,"tryCatchPattern":"try { await createApiKey({ teamId, projectId }); }\ncatch (e) { if (e.message.includes('must belong to team_id')) { await refreshProjectIds(teamId); } else throw e; }","preventionTips":["Always resolve projectId from a team-scoped query, never from user input or cross-team cache","Validate team/project pairing once at config load time","Log both IDs on failure to spot cross-tenant mixing quickly"],"tags":["database","authorization","validation"],"backgroundTag":"record-not-found","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}