diff --git a/src/constants.ts b/src/constants.ts index 567e91d8..18531d15 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -20,6 +20,9 @@ export const headerNames = { hops: 'X-AR-IO-Hops', origin: 'X-AR-IO-Origin', originNodeRelease: 'X-AR-IO-Origin-Node-Release', + digest: 'X-AR-IO-Digest', + stable: 'X-AR-IO-Stable', + verified: 'X-AR-IO-Verified', cache: 'X-Cache', arnsTtlSeconds: 'X-ArNS-TTL-Seconds', arnsResolvedId: 'X-ArNS-Resolved-Id', diff --git a/src/routes/data/handlers.test.ts b/src/routes/data/handlers.test.ts index b8a225a9..b1dfdcc9 100644 --- a/src/routes/data/handlers.test.ts +++ b/src/routes/data/handlers.test.ts @@ -40,9 +40,11 @@ describe('Data routes', () => { beforeEach(() => { app = express(); dataIndex = { + getDataItemAttributes: () => Promise.resolve(undefined), getDataAttributes: () => Promise.resolve(undefined), getDataParent: () => Promise.resolve(undefined), saveDataContentAttributes: () => Promise.resolve(undefined), + getTransactionAttributes: () => Promise.resolve(undefined), }; dataSource = { getData: () => diff --git a/src/routes/data/handlers.ts b/src/routes/data/handlers.ts index d63c220f..9b721149 100644 --- a/src/routes/data/handlers.ts +++ b/src/routes/data/handlers.ts @@ -41,6 +41,27 @@ const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; const REQUEST_METHOD_HEAD = 'HEAD'; +const setDigestStableVerifiedHeaders = ({ + res, + dataAttributes, +}: { + res: Response; + dataAttributes: ContiguousDataAttributes | undefined; +}) => { + if (dataAttributes !== undefined) { + res.setHeader(headerNames.stable, dataAttributes.stable ? 'true' : 'false'); + res.setHeader( + headerNames.verified, + dataAttributes.verified ? 'true' : 'false', + ); + + if (dataAttributes.hash !== undefined) { + res.setHeader(headerNames.digest, dataAttributes.hash); + res.setHeader('ETag', dataAttributes.hash); + } + } +}; + const setDataHeaders = ({ res, dataAttributes, @@ -99,6 +120,8 @@ const setDataHeaders = ({ if (dataAttributes?.contentEncoding !== undefined) { res.header('Content-Encoding', dataAttributes.contentEncoding); } + + setDigestStableVerifiedHeaders({ res, dataAttributes }); }; const getRequestAttributes = (req: Request): RequestAttributes => { @@ -147,6 +170,8 @@ const handleRangeRequest = ( data.sourceContentType ?? 'application/octet-stream'; + setDigestStableVerifiedHeaders({ res, dataAttributes }); + if (isSingleRange) { const totalSize = data.size; const start = ranges[0].start; diff --git a/test/end-to-end/data.test.ts b/test/end-to-end/data.test.ts index cfb0c7d8..f6c37745 100644 --- a/test/end-to-end/data.test.ts +++ b/test/end-to-end/data.test.ts @@ -329,6 +329,16 @@ describe('X-AR-IO headers', function () { resWithHeaders.headers['x-ar-io-origin-node-release'], 'v1.0.0', ); + assert.equal( + resWithHeaders.headers['x-ar-io-digest'], + 'gkOH8JBTdKr_wD9SriwYwCM6p7saQAJFU60AREREQLA', + ); + assert.equal( + resWithHeaders.headers['etag'], + 'gkOH8JBTdKr_wD9SriwYwCM6p7saQAJFU60AREREQLA', + ); + assert.equal(resWithHeaders.headers['x-ar-io-stable'], 'false'); + assert.equal(resWithHeaders.headers['x-ar-io-verified'], 'false'); }); it('Verifying that / for a manifest with a missing index returns no hops, origin and node release', async function () { @@ -454,6 +464,16 @@ describe('X-AR-IO headers', function () { assert.equal(resWithHeaders.headers['x-ar-io-hops'], '6'); assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); assert.equal(resWithHeaders.headers['x-ar-io-origin-node-release'], '10'); + assert.equal( + resWithHeaders.headers['x-ar-io-digest'], + 'gkOH8JBTdKr_wD9SriwYwCM6p7saQAJFU60AREREQLA', + ); + assert.equal( + resWithHeaders.headers['etag'], + 'gkOH8JBTdKr_wD9SriwYwCM6p7saQAJFU60AREREQLA', + ); + assert.equal(resWithHeaders.headers['x-ar-io-stable'], 'false'); + assert.equal(resWithHeaders.headers['x-ar-io-verified'], 'false'); }); it('Verifying that / for a manifest with a missing index returns no hops, origin and node release', async function () {