diff --git a/package-lock.json b/package-lock.json index 26c4717c..23203b40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@apify/consts": "^2.42.0", "@apify/log": "^2.2.6", - "@apify/utilities": "^2.18.0", + "@apify/utilities": "^2.23.2", "@crawlee/types": "^3.3.0", "agentkeepalive": "^4.2.1", "async-retry": "^1.3.3", @@ -165,9 +165,9 @@ "license": "Apache-2.0" }, "node_modules/@apify/utilities": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/@apify/utilities/-/utilities-2.23.0.tgz", - "integrity": "sha512-WAyenlKvtXtvd6V8D2fYwbsmc3dMn3z02JaOhZNx/p8u0NuacNgoLk/+PW4IPWrNxhqgDQZqde4cpNfZOktvsQ==", + "version": "2.23.2", + "resolved": "https://registry.npmjs.org/@apify/utilities/-/utilities-2.23.2.tgz", + "integrity": "sha512-kNA9m7DN+6CW6OEadH43CZPz5dJ1UfimfXtkt2Oq0yU7ovN9kNZXOGrpGPGMxVwKmIeIghZEzMriuI6XVAFtfA==", "license": "Apache-2.0", "dependencies": { "@apify/consts": "^2.47.0", diff --git a/package.json b/package.json index c19c30cb..bbee5514 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "dependencies": { "@apify/consts": "^2.42.0", "@apify/log": "^2.2.6", - "@apify/utilities": "^2.18.0", + "@apify/utilities": "^2.23.2", "@crawlee/types": "^3.3.0", "agentkeepalive": "^4.2.1", "async-retry": "^1.3.3", diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 660ecb20..b83bd377 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ externals: { 'node:util': 'node:util', 'node:zlib': 'node:zlib', + crypto: 'node:crypto', }, }, tools: { diff --git a/src/resource_clients/dataset.ts b/src/resource_clients/dataset.ts index 46b965ed..220b3409 100644 --- a/src/resource_clients/dataset.ts +++ b/src/resource_clients/dataset.ts @@ -1,7 +1,7 @@ import ow from 'ow'; import type { STORAGE_GENERAL_ACCESS } from '@apify/consts'; -import { createStorageContentSignature } from '@apify/utilities'; +import { createStorageContentSignatureAsync } from '@apify/utilities'; import type { ApifyApiError } from '../apify_api_error'; import type { ApiClientSubResourceOptions } from '../base/api_client'; @@ -204,7 +204,7 @@ export class DatasetClient< let createdItemsPublicUrl = new URL(this._publicUrl('items')); if (dataset?.urlSigningSecretKey) { - const signature = createStorageContentSignature({ + const signature = await createStorageContentSignatureAsync({ resourceId: dataset.id, urlSigningSecretKey: dataset.urlSigningSecretKey, expiresInMillis: expiresInSecs ? expiresInSecs * 1000 : undefined, diff --git a/src/resource_clients/key_value_store.ts b/src/resource_clients/key_value_store.ts index 34219fe7..7a4d20a7 100644 --- a/src/resource_clients/key_value_store.ts +++ b/src/resource_clients/key_value_store.ts @@ -5,7 +5,7 @@ import type { JsonValue } from 'type-fest'; import type { STORAGE_GENERAL_ACCESS } from '@apify/consts'; import log from '@apify/log'; -import { createHmacSignature, createStorageContentSignature } from '@apify/utilities'; +import { createHmacSignatureAsync, createStorageContentSignatureAsync } from '@apify/utilities'; import type { ApifyApiError } from '../apify_api_error'; import type { ApiClientSubResourceOptions } from '../base/api_client'; @@ -100,7 +100,7 @@ export class KeyValueStoreClient extends ResourceClient { const recordPublicUrl = new URL(this._publicUrl(`records/${key}`)); if (store?.urlSigningSecretKey) { - const signature = createHmacSignature(store.urlSigningSecretKey, key); + const signature = await createHmacSignatureAsync(store.urlSigningSecretKey, key); recordPublicUrl.searchParams.append('signature', signature); } @@ -138,7 +138,7 @@ export class KeyValueStoreClient extends ResourceClient { let createdPublicKeysUrl = new URL(this._publicUrl('keys')); if (store?.urlSigningSecretKey) { - const signature = createStorageContentSignature({ + const signature = await createStorageContentSignatureAsync({ resourceId: store.id, urlSigningSecretKey: store.urlSigningSecretKey, expiresInMillis: expiresInSecs ? expiresInSecs * 1000 : undefined, diff --git a/test/_helper.js b/test/_helper.js index 61f68e9c..46f48695 100644 --- a/test/_helper.js +++ b/test/_helper.js @@ -10,8 +10,10 @@ class Browser { return this.browser; } - async getInjectedPage(baseUrl, DEFAULT_OPTIONS) { + async getInjectedPage(baseUrl, DEFAULT_OPTIONS, gotoUrl = null) { const page = await this.browser.newPage(); + if (gotoUrl) await page.goto(gotoUrl); + await puppeteerUtils.injectFile(page, `${__dirname}/../dist/bundle.js`); page.on('console', (msg) => console.log(msg.text())); @@ -26,6 +28,7 @@ class Browser { baseUrl, DEFAULT_OPTIONS, ); + return page; } diff --git a/test/datasets.test.js b/test/datasets.test.js index e9c1d160..d204b29d 100644 --- a/test/datasets.test.js +++ b/test/datasets.test.js @@ -19,7 +19,8 @@ describe('Dataset methods', () => { let client; let page; beforeEach(async () => { - page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS); + // Navigate to localhost address to ensure secure context e.g. for Web Crypto API + page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS, baseUrl); client = new ApifyClient({ baseUrl, maxRetries: 0, @@ -406,6 +407,9 @@ describe('Dataset methods', () => { const url = new URL(res); expect(url.searchParams.get('signature')).toBeDefined(); expect(url.pathname).toBe(`/v2/datasets/${datasetId}/items`); + + const browserRes = await page.evaluate((id) => client.dataset(id).createItemsPublicUrl(), datasetId); + expect(browserRes).toEqual(res); }); it('should not include a signature in the URL when the caller lacks permission to access the signing secret key', async () => { @@ -415,6 +419,9 @@ describe('Dataset methods', () => { const url = new URL(res); expect(url.searchParams.get('signature')).toBeNull(); expect(url.pathname).toBe(`/v2/datasets/${datasetId}/items`); + + const browserRes = await page.evaluate((id) => client.dataset(id).createItemsPublicUrl(), datasetId); + expect(browserRes).toEqual(res); }); it('includes provided options (e.g., limit and prefix) as query parameters', async () => { diff --git a/test/key_value_stores.test.js b/test/key_value_stores.test.js index 7a4ba98d..4534abfd 100644 --- a/test/key_value_stores.test.js +++ b/test/key_value_stores.test.js @@ -21,7 +21,8 @@ describe('Key-Value Store methods', () => { let client; let page; beforeEach(async () => { - page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS); + // Navigate to localhost address to ensure secure context e.g. for Web Crypto API + page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS, baseUrl); client = new ApifyClient({ baseUrl, maxRetries: 0, @@ -619,6 +620,12 @@ describe('Key-Value Store methods', () => { const url = new URL(res); expect(url.searchParams.get('signature')).toBeDefined(); expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/records/${key}`); + + const browserRes = await page.evaluate( + ({ storeId, key }) => client.keyValueStore(storeId).getRecordPublicUrl(key), + { storeId, key }, + ); + expect(browserRes).toEqual(res); }); it('should not include a signature in the URL when the caller lacks permission to access the signing secret key', async () => { @@ -629,6 +636,12 @@ describe('Key-Value Store methods', () => { const url = new URL(res); expect(url.searchParams.get('signature')).toBeNull(); expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/records/${key}`); + + const browserRes = await page.evaluate( + ({ storeId, key }) => client.keyValueStore(storeId).getRecordPublicUrl(key), + { storeId, key }, + ); + expect(browserRes).toEqual(res); }); }); @@ -659,6 +672,9 @@ describe('Key-Value Store methods', () => { const url = new URL(res); expect(url.searchParams.get('signature')).toBeDefined(); expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/keys`); + + const browserRes = await page.evaluate((id) => client.keyValueStore(id).createKeysPublicUrl(), storeId); + expect(browserRes).toEqual(res); }); it('should not include a signature in the URL when the caller lacks permission to access the signing secret key', async () => { @@ -668,6 +684,9 @@ describe('Key-Value Store methods', () => { const url = new URL(res); expect(url.searchParams.get('signature')).toBeNull(); expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/keys`); + + const browserRes = await page.evaluate((id) => client.keyValueStore(id).createKeysPublicUrl(), storeId); + expect(browserRes).toEqual(res); }); it('includes provided options (e.g., limit and prefix) as query parameters', async () => {