fix(memory): scope InsightsRepo operations to userId — M2-001/002
Security audit findings and fixes: M2-001 — searchByEmbedding: confirmed already user-scoped via WHERE user_id M2-002 — findByUser: confirmed already user-scoped M2-002 — decayOldInsights: was global (no userId filter); now requires userId param and scopes UPDATE to eq(insights.userId, userId). Added decayAllInsights as a separate system-only method for cron tier management. Additional unscoped operations fixed: - findById: added userId param + AND eq(userId) to prevent cross-user read - update: added userId param + AND eq(userId) to prevent cross-user write - remove: added userId param + AND eq(userId) to prevent cross-user delete - memory.controller getInsight/removeInsight: now pass user.id for ownership - summarization.service: switched tier-management cron to decayAllInsights Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,11 @@ export function createInsightsRepo(db: Db) {
|
||||
.limit(limit);
|
||||
},
|
||||
|
||||
async findById(id: string): Promise<Insight | undefined> {
|
||||
const rows = await db.select().from(insights).where(eq(insights.id, id));
|
||||
async findById(id: string, userId: string): Promise<Insight | undefined> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(insights)
|
||||
.where(and(eq(insights.id, id), eq(insights.userId, userId)));
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
@@ -29,17 +32,24 @@ export function createInsightsRepo(db: Db) {
|
||||
return rows[0]!;
|
||||
},
|
||||
|
||||
async update(id: string, data: Partial<NewInsight>): Promise<Insight | undefined> {
|
||||
async update(
|
||||
id: string,
|
||||
userId: string,
|
||||
data: Partial<NewInsight>,
|
||||
): Promise<Insight | undefined> {
|
||||
const rows = await db
|
||||
.update(insights)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(insights.id, id))
|
||||
.where(and(eq(insights.id, id), eq(insights.userId, userId)))
|
||||
.returning();
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
async remove(id: string): Promise<boolean> {
|
||||
const rows = await db.delete(insights).where(eq(insights.id, id)).returning();
|
||||
async remove(id: string, userId: string): Promise<boolean> {
|
||||
const rows = await db
|
||||
.delete(insights)
|
||||
.where(and(eq(insights.id, id), eq(insights.userId, userId)))
|
||||
.returning();
|
||||
return rows.length > 0;
|
||||
},
|
||||
|
||||
@@ -70,8 +80,33 @@ export function createInsightsRepo(db: Db) {
|
||||
|
||||
/**
|
||||
* Decay relevance scores for old insights that haven't been accessed recently.
|
||||
* Scoped to a specific user to prevent cross-user data mutation.
|
||||
*/
|
||||
async decayOldInsights(olderThan: Date, decayFactor = 0.95): Promise<number> {
|
||||
async decayOldInsights(userId: string, olderThan: Date, decayFactor = 0.95): Promise<number> {
|
||||
const result = await db
|
||||
.update(insights)
|
||||
.set({
|
||||
relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`,
|
||||
decayedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(insights.userId, userId),
|
||||
lt(insights.updatedAt, olderThan),
|
||||
sql`${insights.relevanceScore} > 0.1`,
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
return result.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decay relevance scores for all users' old insights.
|
||||
* This is a system-level maintenance operation intended for scheduled cron jobs only.
|
||||
* Do NOT expose this through user-facing API endpoints.
|
||||
*/
|
||||
async decayAllInsights(olderThan: Date, decayFactor = 0.95): Promise<number> {
|
||||
const result = await db
|
||||
.update(insights)
|
||||
.set({
|
||||
|
||||
Reference in New Issue
Block a user