diff --git a/src/client.ts b/src/client.ts index 6ba1a6d..9bd980a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -97,14 +97,12 @@ export class Client { } const apiHeaders: Record = { authorization: `Bearer ${this.token}` } - const url = new URL(`/api/v1/sites/${this.siteID}/blobs`, this.apiURL ?? 'https://api.netlify.com') + const url = new URL(`/api/v1/blobs/${this.siteID}/${storeName}`, this.apiURL ?? 'https://api.netlify.com') for (const key in parameters) { url.searchParams.set(key, parameters[key]) } - url.searchParams.set('context', storeName) - // If there is no key, we're dealing with the list endpoint, which is // implemented directly in the Netlify API. if (key === undefined) { diff --git a/src/list.test.ts b/src/list.test.ts index beb3dc0..4144e2f 100644 --- a/src/list.test.ts +++ b/src/list.test.ts @@ -59,7 +59,7 @@ describe('list', () => { next_cursor: 'cursor_1', }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -83,7 +83,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?cursor=cursor_1&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?cursor=cursor_1`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -100,7 +100,7 @@ describe('list', () => { directories: [], }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?cursor=cursor_2&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?cursor=cursor_2`, }) globalThis.fetch = mockStore.fetch @@ -148,7 +148,7 @@ describe('list', () => { next_cursor: 'cursor_1', }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?directories=true&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?directories=true`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -172,7 +172,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?directories=true&cursor=cursor_1&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?directories=true&cursor=cursor_1`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -189,7 +189,7 @@ describe('list', () => { directories: ['dir3'], }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?directories=true&cursor=cursor_2&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?directories=true&cursor=cursor_2`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -206,7 +206,7 @@ describe('list', () => { directories: [], }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?prefix=dir2%2F&directories=true&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?prefix=dir2%2F&directories=true`, }) globalThis.fetch = mockStore.fetch @@ -258,7 +258,7 @@ describe('list', () => { ], }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?prefix=group%2F&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?prefix=group%2F`, }) globalThis.fetch = mockStore.fetch @@ -303,7 +303,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -319,7 +319,7 @@ describe('list', () => { ], }), ), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs?cursor=cursor_2&context=${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?cursor=cursor_2`, }) globalThis.fetch = mockStore.fetch diff --git a/src/main.test.ts b/src/main.test.ts index 140f5d5..464c3c3 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -47,7 +47,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response(value), @@ -56,7 +56,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response(value), @@ -65,7 +65,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${complexKey}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${complexKey}`, }) .get({ response: new Response(value), @@ -97,7 +97,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response('Something went wrong', { status: 404 }), @@ -120,7 +120,7 @@ describe('get', () => { const mockStore = new MockFetch().get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 401 }), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -142,7 +142,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response('Something went wrong', { status: 401 }), @@ -361,7 +361,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { headers }), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -383,7 +383,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 404 }), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -406,7 +406,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { headers }), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -476,7 +476,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response(value, { headers: responseHeaders }), @@ -485,7 +485,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response(value, { headers: responseHeaders }), @@ -518,7 +518,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response('Something went wrong', { status: 404 }), @@ -546,7 +546,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ response: new Response(value, { headers: responseHeaders }), @@ -585,7 +585,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: `${signedURL}b` })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ headers: { 'if-none-match': etags.wrong }, @@ -595,7 +595,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: `${signedURL}a` })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .get({ headers: { 'if-none-match': etags.right }, @@ -679,7 +679,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .put({ body: value, @@ -690,7 +690,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${complexKey}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${complexKey}`, }) .put({ body: value, @@ -724,7 +724,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}`, 'netlify-blobs-metadata': encodedMetadata }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .put({ body: value, @@ -753,7 +753,7 @@ describe('set', () => { const mockStore = new MockFetch().put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 401 }), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -795,7 +795,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .put({ body: value, @@ -949,7 +949,7 @@ describe('setJSON', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .put({ body: JSON.stringify({ value }), @@ -1058,7 +1058,7 @@ describe('delete', () => { .delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .delete({ response: new Response(null), @@ -1067,7 +1067,7 @@ describe('delete', () => { .delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${complexKey}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${complexKey}`, }) .delete({ response: new Response(null), @@ -1093,7 +1093,7 @@ describe('delete', () => { .delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) .delete({ response: new Response(null, { status: 404 }), @@ -1117,7 +1117,7 @@ describe('delete', () => { const mockStore = new MockFetch().delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 401 }), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1244,7 +1244,7 @@ describe('Deploy scope', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=deploy%3A${deployID}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/deploy:${deployID}/${key}`, }) .get({ response: new Response(value), @@ -1253,7 +1253,7 @@ describe('Deploy scope', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=deploy%3A${deployID}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/deploy:${deployID}/${key}`, }) .get({ response: new Response(value), @@ -1314,7 +1314,7 @@ describe('Deploy scope', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=deploy%3A${deployID}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/deploy:${deployID}/${key}`, }) .get({ response: new Response(value), @@ -1323,7 +1323,7 @@ describe('Deploy scope', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=deploy%3A${deployID}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/deploy:${deployID}/${key}`, }) .get({ response: new Response(value), diff --git a/src/retry.ts b/src/retry.ts index 336a6b5..9d6f8f2 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -1,6 +1,8 @@ +import { env } from 'node:process' + import type { Fetcher } from './types.ts' -const DEFAULT_RETRY_DELAY = 5000 +const DEFAULT_RETRY_DELAY = env.NODE_ENV === 'test' ? 1 : 5000 const MIN_RETRY_DELAY = 1000 const MAX_RETRY = 5 const RATE_LIMIT_HEADER = 'X-RateLimit-Reset' diff --git a/src/server.ts b/src/server.ts index a1041d3..cd13e20 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,8 +12,7 @@ import { decodeMetadata, encodeMetadata, METADATA_HEADER_INTERNAL } from './meta import { HTTPMethod } from './types.ts' import { isNodeError, Logger } from './util.ts' -const API_URL_PATH = /\/api\/v1\/sites\/(?[^/]+)\/blobs\/?(?[^?]*)/ -const DEFAULT_STORE = 'production' +const API_URL_PATH = /\/api\/v1\/blobs\/(?[^/]+)\/(?[^/]+)\/?(?[^?]*)/ export enum Operation { DELETE = 'delete', @@ -367,10 +366,9 @@ export class BlobsServer { return null } - const fullURL = new URL(req.url, this.address) - const storeName = fullURL.searchParams.get('context') ?? DEFAULT_STORE const key = apiURLMatch.groups?.key const siteID = apiURLMatch.groups?.site_id as string + const storeName = apiURLMatch.groups?.store_name as string const urlPath = [siteID, storeName, key].filter(Boolean) as string[] const url = new URL(`/${urlPath.join('/')}?signature=${this.tokenHash}`, this.address)