From 67ac2bbc7ca80cc790c270ed7fe8a0711562db6d Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 7 Aug 2026 20:57:07 +0200 Subject: [PATCH] fix: Redis expire guard, transferOwnership prev-owner, SVG regex - Redis get() rejects legacy expire=0 entries and cleans them up - transferOwnership invalidates previous owner in addition to new - SVG regex uses non-greedy match and handles self-closing - SVG fallback returns empty string instead of HTML wrapper - Memory channel comment: FIFO eviction, not timestamp-based --- apps/nextjs/src/app/api/user-medias/svg-purify.ts | 4 ++-- packages/api/src/router/group.ts | 9 +++++++++ packages/next-cache-handler/src/redis-handler.ts | 5 +++++ packages/redis/src/lib/memory-channel.ts | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/nextjs/src/app/api/user-medias/svg-purify.ts b/apps/nextjs/src/app/api/user-medias/svg-purify.ts index 06d7d8501..c5237538a 100644 --- a/apps/nextjs/src/app/api/user-medias/svg-purify.ts +++ b/apps/nextjs/src/app/api/user-medias/svg-purify.ts @@ -86,8 +86,8 @@ export const sanitizeSvg = (svgText: string) => { // linkedom's DOMParser omits html/body for fragment-only SVG; DOMPurify needs a body root. const wrapped = `${svgText}`; const sanitized = purify.sanitize(wrapped, svgSanitizeOptions); - const svgMatch = sanitized.match(//i); - return svgMatch ? svgMatch[0] : sanitized; + const svgMatch = sanitized.match(/|\/>)/i); + return svgMatch ? svgMatch[0] : ""; }; export { svgSanitizeOptions, purify }; diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index 7570a8ee0..0041bce5d 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -327,13 +327,22 @@ export const groupRouter = createTRPCRouter({ await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); + const group = await ctx.db.query.groups.findFirst({ + columns: { ownerId: true }, + where: eq(groups.id, input.groupId), + }); + await ctx.db .update(groups) .set({ ownerId: input.userId, }) .where(eq(groups.id, input.groupId)); + invalidateUserCache(input.userId); + if (group?.ownerId && group.ownerId !== input.userId) { + invalidateUserCache(group.ownerId); + } }), deleteGroup: permissionRequiredProcedure .requiresPermission("admin") diff --git a/packages/next-cache-handler/src/redis-handler.ts b/packages/next-cache-handler/src/redis-handler.ts index 30415e153..8b02b1e4b 100644 --- a/packages/next-cache-handler/src/redis-handler.ts +++ b/packages/next-cache-handler/src/redis-handler.ts @@ -56,6 +56,11 @@ export class RedisCacheHandler implements CacheHandler { body: number[]; }; + if (parsed.expire <= 0) { + await this.redis.del(this.entryKey(cacheKey)).catch(() => {}); + return undefined; + } + const allTags = [...parsed.tags, ...softTags]; const expiration = await this.getExpiration(allTags); if (expiration !== 0 && expiration > parsed.timestamp) { diff --git a/packages/redis/src/lib/memory-channel.ts b/packages/redis/src/lib/memory-channel.ts index 16975a3bb..34b07d4dd 100644 --- a/packages/redis/src/lib/memory-channel.ts +++ b/packages/redis/src/lib/memory-channel.ts @@ -12,7 +12,7 @@ export const memoryPublish = (channel: string, data: unknown) => { const serialized = superjson.stringify(data); lastData.set(channel, { data: serialized, timestamp: Date.now() }); - // Bounded: evict oldest if over limit + // ponytail: FIFO eviction by Map insertion order, not timestamp. if (lastData.size > MAX_ENTRIES) { const oldest = lastData.keys().next().value; if (oldest) lastData.delete(oldest);