@@ -3,6 +3,7 @@ import { ROOM_MEMBERSHIP_ACTIONS, satisfiesRoomMembership } from '@sim/platform-
33import type { AccessRevokedBroadcast } from '@sim/realtime-protocol/events'
44import { FILE_DOC_EVENTS , type FileDocPermission } from '@sim/realtime-protocol/file-doc'
55import {
6+ isProjectRoom ,
67 parseRoomName ,
78 projectFileDocTarget ,
89 ROOM_TYPES ,
@@ -35,14 +36,8 @@ const logger = createLogger('AccessRevalidation')
3536export const ACCESS_REVALIDATION_SWEEP_INTERVAL_MS = ROLE_REVALIDATION_TTL_MS
3637
3738/**
38- * Fallback consumed by {@link resolveCurrentRoomPermission} only on a transient DB
39- * failure with a cold cache, where resolving to a permission that KEEPS the socket
40- * (never eviction) is the safe outcome during an outage. It is the room's own
41- * membership level, so it satisfies that room's requirement exactly — a static
42- * `'read'` would have evicted every file-doc socket (which needs `write`) on a cold
43- * cache blip. The scan deliberately does not read presence for a per-socket
44- * join-time role: that would put a Redis dependency inside the security-critical
45- * scan lane, and the fallback's only job is to be permissive.
39+ * Preserves membership for room types that allow a cold-cache outage fallback.
40+ * Document rooms throw instead; the sweep retains their subscriptions for retry.
4641 */
4742function fallbackRoleFor ( type : RoomType ) : string {
4843 return ROOM_MEMBERSHIP_ACTIONS [ type ]
@@ -329,15 +324,21 @@ export function startAccessRevalidationSweep(roomManager: IRoomManager): AccessR
329324 // resolution keeps running in the background and is re-raced when the
330325 // rotation returns to this socket, so it is acted on once it settles.
331326 const role = await Promise . race ( [
332- room . type === ROOM_TYPES . PROJECT_FILE_DOC
327+ isProjectRoom ( room )
333328 ? resolveCurrentRoomPermission ( userId , room , fallbackRoleFor ( room . type ) , socket . id )
334329 : resolveCurrentRoomPermission ( userId , room , fallbackRoleFor ( room . type ) ) ,
335330 sleep ( Math . min ( SCAN_SOCKET_TIMEOUT_MS , remainingBudget ) ) . then ( ( ) => SCAN_TIMED_OUT ) ,
336331 ] )
337332 // {@link SCAN_TIMED_OUT } is the only symbol this race can yield; matching on
338333 // the type narrows it out of the permission comparison below.
339- if ( typeof role !== 'symbol' && room . type === ROOM_TYPES . PROJECT_FILE_DOC ) {
340- const target = projectFileDocTarget ( room )
334+ if (
335+ typeof role !== 'symbol' &&
336+ ( room . type === ROOM_TYPES . PROJECT_FILE_DOC || room . type === ROOM_TYPES . WORKSPACE_FILE_DOC )
337+ ) {
338+ const target =
339+ room . type === ROOM_TYPES . PROJECT_FILE_DOC
340+ ? projectFileDocTarget ( room )
341+ : { fileId : room . id }
341342 if ( target )
342343 socket . emit ( FILE_DOC_EVENTS . PERMISSION , {
343344 ...target ,
@@ -349,9 +350,6 @@ export function startAccessRevalidationSweep(roomManager: IRoomManager): AccessR
349350 `Authorization check timed out for user ${ userId } on ${ name } ; skipping this pass`
350351 )
351352 } else if ( ! satisfiesRoomMembership ( role , room . type ) ) {
352- // Covers both revocation (`null`) and downgrade below the level this room
353- // requires — a member dropped to `read` may keep a table room but not the
354- // collaborative document editor, exactly as at join time.
355353 revokeSocket ( socket , room , name )
356354 }
357355 } catch ( error ) {
0 commit comments