From b8e3ce12c7eb0f486d9e2b9c5f3caa5451351d8f Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 24 Nov 2025 14:39:08 -0300 Subject: [PATCH 1/2] fix: skip egress tracking if content found in multiple spaces --- src/middleware/withAuthorizedSpace.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/middleware/withAuthorizedSpace.js b/src/middleware/withAuthorizedSpace.js index daf49cf..55a5019 100644 --- a/src/middleware/withAuthorizedSpace.js +++ b/src/middleware/withAuthorizedSpace.js @@ -104,6 +104,15 @@ export function withAuthorizedSpace (handler) { .map((site) => extractSpaceDID(site.space)) .filter((space) => space !== undefined) + // If content is found in multiple DIFFERENT spaces, skip egress tracking + // by not setting ctx.space (security/billing concern - ambiguous ownership) + const uniqueSpaces = [...new Set(spaces.map(s => s.toString()))] + const skipEgressTracking = uniqueSpaces.length > 1 + if (skipEgressTracking && env.DEBUG === 'true') { + console.log(`Content found in ${uniqueSpaces.length} different spaces - egress tracking will be skipped`) + console.log(`Spaces: ${uniqueSpaces.join(', ')}`) + } + try { // First space to successfully authorize is the one we'll use. const { space: selectedSpace, delegationProofs } = await Promise.any( @@ -117,7 +126,8 @@ export function withAuthorizedSpace (handler) { ) return handler(request, env, { ...ctx, - space: SpaceDID.from(selectedSpace.toString()), + // Only set space if we're not skipping egress tracking + space: skipEgressTracking ? undefined : SpaceDID.from(selectedSpace.toString()), delegationProofs, locator: locator.scopeToSpaces([selectedSpace]) }) From 0642d3e782e2dc2e9877f86fbebf59c68c5122dd Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 24 Nov 2025 15:11:12 -0300 Subject: [PATCH 2/2] added tests --- .../middleware/withAuthorizedSpace.spec.js | 74 ++++++++++++++++--- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/test/unit/middleware/withAuthorizedSpace.spec.js b/test/unit/middleware/withAuthorizedSpace.spec.js index 8bcddcd..133ff2a 100644 --- a/test/unit/middleware/withAuthorizedSpace.spec.js +++ b/test/unit/middleware/withAuthorizedSpace.spec.js @@ -326,12 +326,13 @@ describe('withAuthorizedSpace', async () => { { ...ctx, authToken: 'space1-token', delegationProofs: [], gatewaySigner } ) - expect(await response1.json()).to.deep.equal({ - CID: cid.toString(), - Space: space1.did(), - Token: 'space1-token', - URLs: ['http://example.com/1/blob'] - }) + // When content is in multiple spaces, ctx.space is undefined (egress tracking skipped) + // Note: JSON.stringify removes undefined values, so Space won't be in the response + const result1 = await response1.json() + expect(result1.CID).to.equal(cid.toString()) + expect(result1.Space).to.be.undefined + expect(result1.Token).to.equal('space1-token') + expect(result1.URLs).to.deep.equal(['http://example.com/1/blob']) const error2 = await rejection( withAuthorizedSpace(sinon.fake(innerHandler))( @@ -357,11 +358,64 @@ describe('withAuthorizedSpace', async () => { { ...ctx, authToken: 'space3-token', delegationProofs: [], gatewaySigner } ) - expect(await response3.json()).to.deep.equal({ + // When content is in multiple spaces, ctx.space is undefined (egress tracking skipped) + // Note: JSON.stringify removes undefined values, so Space won't be in the response + const result3 = await response3.json() + expect(result3.CID).to.equal(cid.toString()) + expect(result3.Space).to.be.undefined + expect(result3.Token).to.equal('space3-token') + expect(result3.URLs).to.deep.equal(['http://example.com/3/blob']) + }) + + it('should NOT skip egress tracking when content is in a single space', async () => { + const cid = createTestCID(0) + const space1 = await ed25519.Signer.generate() + + const ctx = { + ...context, + dataCid: cid, + locator: createLocator(cid.multihash, { + ok: { + digest: cid.multihash, + site: [ + { + location: [new URL('http://example.com/1/blob')], + range: { offset: 0, length: 100 }, + space: space1.did() + }, + { + location: [new URL('http://example.com/2/blob')], + range: { offset: 0, length: 100 }, + space: space1.did() // Same space, different location + } + ] + }, + error: undefined + }), + delegationsStorage: createDelegationStorage([ + await serve.transportHttp.delegate({ + issuer: space1, + audience: gatewayIdentity, + with: space1.did(), + nb: { token: 'space1-token' } + }) + ]), + gatewayIdentity + } + + const response = await withAuthorizedSpace(innerHandler)( + request, + {}, + { ...ctx, authToken: 'space1-token', delegationProofs: [], gatewaySigner } + ) + + // When content is in a single space (even with multiple sites), ctx.space should be set + const result = await response.json() + expect(result).to.deep.equal({ CID: cid.toString(), - Space: space3.did(), - Token: 'space3-token', - URLs: ['http://example.com/3/blob'] + Space: space1.did(), + Token: 'space1-token', + URLs: ['http://example.com/1/blob', 'http://example.com/2/blob'] }) })