From b2022e2492a998dce54c150916b541e7269bc102 Mon Sep 17 00:00:00 2001 From: jacksonriding <99007683+jacksonriding@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:44:33 +1000 Subject: [PATCH] fix: avoid duplicate restoration of explicit directory markers --- service/src/egress-gateway.test.ts | 39 ++++++++++++++++++++++++++++++ service/src/egress-gateway.ts | 5 ++++ 2 files changed, 44 insertions(+) diff --git a/service/src/egress-gateway.test.ts b/service/src/egress-gateway.test.ts index a3f81f51..66baf586 100644 --- a/service/src/egress-gateway.test.ts +++ b/service/src/egress-gateway.test.ts @@ -664,6 +664,45 @@ describe('egress gateway routes', () => { expect(header(upstreamCalls[0].init, EGRESS_GRANT_HEADER)).toBeUndefined(); }); + test('does not auto-restore an explicitly referenced marker again under its unsanitized name', async () => { + const grant = claims({ + input_files: [ + { id: 'marker_explicit', session_id: 'sess_input', name: 'tps_extract/_.dirkeep-dd1fdd' }, + { id: 'file_123', session_id: 'sess_input', name: 'tps_extract/chapter.pdf' }, + // An identical object id from another session must not suppress this session's marker. + { id: 'marker_inherited', session_id: 'sess_other', name: 'tps_extract/sub/_.dirkeep-alias' }, + { id: 'file_sub', session_id: 'sess_input', name: 'tps_extract/sub/data.txt' }, + ], + read_sessions: ['sess_input', 'sess_other'], + }); + upstreamResponse = Response.json([ + { id: 'marker_explicit', name: 'tps_extract/.dirkeep', storage_session_id: 'sess_input' }, + { id: 'file_123', name: 'tps_extract/chapter.pdf', storage_session_id: 'sess_input' }, + { id: 'marker_inherited', name: 'tps_extract/sub/.dirkeep', storage_session_id: 'sess_input' }, + { id: 'private_marker', name: 'private/.dirkeep', storage_session_id: 'sess_input' }, + ]); + const readSession = sessionHandle({ dir: 'read', sessionId: 'sess_input' }); + const response = await gatewayFetch(`/sessions/${readSession}/objects?detail=normalized`, { + headers: grantHeader(grant), + }); + expect(response.status).toBe(200); + const body = await response.json() as Array<{ name: string }>; + expect(body.map(file => file.name)).toEqual(['tps_extract/chapter.pdf', 'tps_extract/sub/.dirkeep']); + + // The explicit reference still downloads its original bytes, even if the + // user's file named .dirkeep is non-empty. Suppress only the extra listing. + upstreamResponse = new Response('user marker content', { + headers: { 'Content-Disposition': 'attachment; filename="tps_extract/.dirkeep"' }, + }); + const marker = objectHandle({ fileId: 'marker_explicit', name: 'tps_extract/_.dirkeep-dd1fdd' }); + const download = await gatewayFetch(`/sessions/${readSession}/objects/${marker}`, { + headers: grantHeader(grant), + }); + expect(download.status).toBe(200); + expect(await download.text()).toBe('user marker content'); + expect(download.headers.get('content-disposition')).toContain('tps_extract/.dirkeep'); + }); + test('accepts legacy rollout grants and handles while ledger-required mode is enabled', async () => { const redis = await startTestRedis(); env.EGRESS_LEDGER_REQUIRED = true; diff --git a/service/src/egress-gateway.ts b/service/src/egress-gateway.ts index 2af379c1..b4cd3a05 100644 --- a/service/src/egress-gateway.ts +++ b/service/src/egress-gateway.ts @@ -556,6 +556,9 @@ app.get('/sessions/:sessionHandle/objects', async (req, res) => { const allowedFiles = inputFileSet(grant); const allowedDirkeeps = requiredDirkeepSet(grant); + const explicitObjectIds = new Set(grant.input_files + .filter(file => file.session_id === sessionId) + .map(file => file.id)); const now = Math.floor(Date.now() / 1000); const normalized = data .filter((obj): obj is { id: string; name: string; storage_session_id: string } => ( @@ -572,6 +575,8 @@ app.get('/sessions/:sessionHandle/objects', async (req, res) => { allowedFiles, allowedDirkeeps, )) + // Avoid auto-restoring a renamed explicit marker twice at its stored path. + .filter(obj => !isDirkeepName(obj.name) || !explicitObjectIds.has(obj.id)) .map(obj => ({ ...obj, storage_session_id: req.params.sessionHandle,