45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
// server/supabase/functions/audio-purge/policy.ts
|
|
// Pure rules for which queued storage keys the service-role worker may remove.
|
|
// storage_key is client-written (RLS only pins its first segment), so the
|
|
// worker re-checks it before acting with service-role storage access.
|
|
|
|
export const AUDIO_BUCKET = 'audio'
|
|
export const MAX_STORAGE_KEY_LENGTH = 1024
|
|
|
|
export interface PurgeQueueEntry {
|
|
id: number
|
|
userId: string
|
|
storageKey: string
|
|
leaseToken: string
|
|
}
|
|
|
|
export interface PurgePlan {
|
|
/** Entries whose object may be removed from the audio bucket. */
|
|
removable: PurgeQueueEntry[]
|
|
/** Entries dropped without touching storage (key outside the owner's prefix). */
|
|
rejected: PurgeQueueEntry[]
|
|
}
|
|
|
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
// deno-lint-ignore no-control-regex
|
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/
|
|
|
|
/** True when key is a plain object path inside the owner's own audio prefix. */
|
|
export function isPurgeableStorageKey(userId: string, key: string): boolean {
|
|
if (!UUID_PATTERN.test(userId)) return false
|
|
if (key.length === 0 || key.length > MAX_STORAGE_KEY_LENGTH) return false
|
|
if (key.includes('\\') || CONTROL_CHARACTERS.test(key)) return false
|
|
const segments = key.split('/')
|
|
if (segments.length < 2 || segments[0] !== userId) return false
|
|
return segments.every((segment) => segment.length > 0 && segment !== '.' && segment !== '..')
|
|
}
|
|
|
|
export function planPurge(entries: readonly PurgeQueueEntry[]): PurgePlan {
|
|
const removable: PurgeQueueEntry[] = []
|
|
const rejected: PurgeQueueEntry[] = []
|
|
for (const entry of entries) {
|
|
if (isPurgeableStorageKey(entry.userId, entry.storageKey)) removable.push(entry)
|
|
else rejected.push(entry)
|
|
}
|
|
return { removable, rejected }
|
|
}
|