diff --git a/src/client.ts b/src/client.ts index 619f8be..ffcf94c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,6 +4,8 @@ import { encodeMetadata, Metadata, METADATA_HEADER_EXTERNAL, METADATA_HEADER_INT import { fetchAndRetry } from './retry.ts' import { BlobInput, Fetcher, HTTPMethod } from './types.ts' +export const SIGNED_URL_ACCEPT_HEADER = 'application/json;type=signed-url' + interface MakeStoreRequestOptions { body?: BlobInput | null consistency?: ConsistencyMode @@ -135,7 +137,7 @@ export class Client { } const res = await this.fetch(url.toString(), { - headers: { ...apiHeaders, accept: `application/json;type=signed-url` }, + headers: { ...apiHeaders, accept: SIGNED_URL_ACCEPT_HEADER }, method, }) diff --git a/src/server.test.ts b/src/server.test.ts index 68692c7..5d1dd3e 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -366,3 +366,60 @@ test('Lists site stores', async () => { expect(stores).toStrictEqual(['coldplay', 'phoenix']) }) + +test('Returns a signed URL or the blob directly based on the request parameters', async () => { + const siteID = '9a003659-aaaa-0000-aaaa-63d3720d8621' + const token = 'some token' + const value = 'value 1' + const directory = await tmp.dir() + const server = new BlobsServer({ + directory: directory.path, + token, + }) + + const { port } = await server.start() + const store = getStore({ + edgeURL: `http://localhost:${port}`, + name: 'my-store', + token, + siteID, + }) + + await store.set('key-1', value) + + // When reading through a legacy API endpoint, we should get a signed URL. + const res1 = await fetch(`http://localhost:${port}/api/v1/sites/${siteID}/blobs/key-1?context=site:my-store`, { + headers: { + authorization: `Bearer ${token}`, + }, + }) + const { url: url1 } = await res1.json() + const data1 = await fetch(url1) + + expect(await data1.text()).toBe(value) + + // When reading through a new API endpoint, we should get the blob data by + // default. + const res2 = await fetch(`http://localhost:${port}/api/v1/blobs/${siteID}/site:my-store/key-1`, { + headers: { + authorization: `Bearer ${token}`, + }, + }) + expect(await res2.text()).toBe(value) + + // When reading through a new API endpoint and requesting a signed URL, we + // should get one. + const res3 = await fetch(`http://localhost:${port}/api/v1/blobs/${siteID}/site:my-store/key-1`, { + headers: { + accept: 'application/json;type=signed-url', + authorization: `Bearer ${token}`, + }, + }) + const { url: url3 } = await res3.json() + const data3 = await fetch(url3) + + expect(await data3.text()).toBe(value) + + await server.stop() + await fs.rm(directory.path, { force: true, recursive: true }) +}) diff --git a/src/server.ts b/src/server.ts index d55c2f2..91a479f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,6 +8,7 @@ import stream from 'node:stream' import { promisify } from 'node:util' import { ListResponse } from './backend/list.ts' +import { SIGNED_URL_ACCEPT_HEADER } from './client.ts' import { decodeMetadata, encodeMetadata, METADATA_HEADER_INTERNAL } from './metadata.ts' import { HTTPMethod } from './types.ts' import { isNodeError, Logger } from './util.ts' @@ -137,11 +138,11 @@ export class BlobsServer { const apiMatch = this.parseAPIRequest(req) const url = apiMatch?.url ?? new URL(req.url ?? '', this.address) - if (apiMatch?.key) { + if (apiMatch?.key && apiMatch?.useSignedURL) { return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })) } - const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(url) + const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(apiMatch?.url ?? url) // If there's no root path, the request is invalid. if (!rootPath) { @@ -404,7 +405,7 @@ export class BlobsServer { siteID, storeName, url, - useSignedURL: req.headers.accept === 'application/json;type=signed-url', + useSignedURL: req.headers.accept === SIGNED_URL_ACCEPT_HEADER, } } @@ -481,9 +482,8 @@ export class BlobsServer { } const { authorization = '' } = req.headers - const parts = authorization.split(' ') - if (parts.length === 2 || (parts[0].toLowerCase() === 'bearer' && parts[1] === this.token)) { + if (authorization.toLowerCase().startsWith('bearer ') && authorization.slice('bearer '.length) === this.token) { return true }