{"record":{"id":"8a96bec8ab8a7a2f","repo":"thedotmack/claude-mem","slug":"server-session-id-must-belong-to-project-id-and-te","errorCode":null,"errorMessage":"server_session_id must belong to project_id and team_id","messagePattern":"server_session_id must belong to project_id and team_id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/storage/postgres/utils.ts","lineNumber":78,"sourceCode":"  );\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}\n\nexport function canonicalJson(value: unknown): string {\n  return JSON.stringify(sortJson(value));\n}\n\nexport function deterministicKey(parts: readonly unknown[]): string {\n  const fingerprint = createHash('sha256')\n    .update(canonicalJson(parts))\n    .digest('hex');\n  return fingerprint;\n}\n\nfunction sortJson(value: unknown): unknown {\n  if (Array.isArray(value)) {\n    return value.map(sortJson);\n  }","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/utils.ts#L60-L96","documentation":"assertSessionOwnership verifies that a server_session_id belongs to both the supplied project and team before writes proceed. The SELECT matches on id, project_id, and team_id simultaneously; any mismatch means the session is not scoped to that project/team and the error is thrown. It enforces the full three-level ownership chain (team -> project -> session).","triggerScenarios":"Calling create, validateSource, or addSource with a serverSessionId that belongs to a different project, a different team, or that no longer exists.","commonSituations":"Reusing a session ID across projects; passing a session ID from a dev database to a prod team; sessions deleted between listing and use; mixing IDs from two tenants in one request.","solutions":["Confirm the server_session_id was created under the same projectId and teamId in the request","Re-query server_sessions for the project and use a live session ID","Check for cross-environment (dev/staging/prod) configuration mixing","Recreate the session under the correct project if it was deleted"],"exampleFix":"// before\nawait addSource({ teamId, projectId: 'projA', serverSessionId: sessionOfProjB });\n// after\nconst sessions = await listSessions(teamId, 'projA');\nawait addSource({ teamId, projectId: 'projA', serverSessionId: sessions[0].id });","handlingStrategy":"validation","validationCode":"const row = await queryOne('SELECT id FROM server_sessions WHERE id = $1 AND project_id = $2 AND team_id = $3', [serverSessionId, projectId, teamId]);\nif (!row) throw new Error('session not in project/team');","typeGuard":null,"tryCatchPattern":"try { await addSource(args); }\ncatch (e) { if (e.message.includes('server_session_id must belong')) { args.serverSessionId = await getDefaultSession(args.teamId, args.projectId); await addSource(args); } else throw e; }","preventionTips":["Keep session IDs together with their project/team scope in one config object","Never share session IDs across environments","Refresh session IDs after any project migration or deletion"],"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"}