Skip to content

Commit 72a1d3c

Browse files
authored
perf: don't bundle node:crypto in the browser bundle (#782)
Uses new implementations of `createStorageContentSignature` and `createHmacSignature` from `@apify/utilities`. Those native WebCrypto API instead of importing the `node:crypto` package, so we can mark `crypto` as external in the browser bundler and cut the bundle size ~2.4 times (both plain and compressed). |before (`master`)|after (`perf/use-native-crypto`)| |---|---| |<img width="362" height="195" alt="image" src="https://github.com/user-attachments/assets/1ae1b104-2c6a-42e3-bfda-e6d63f29d1b0" /> | <img width="362" height="195" alt="image" src="https://github.com/user-attachments/assets/246a1ae4-0ace-4fd5-aed7-34639ff989e9" /> | **Note:** `SubtleCrypto` API in browsers is only available in [Secure Contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). This should be fairly safe (as any `https://`-served page is one, as well as `localhost`). Related to #753
1 parent cf3486e commit 72a1d3c

File tree

8 files changed

+43
-13
lines changed

8 files changed

+43
-13
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"dependencies": {
6565
"@apify/consts": "^2.42.0",
6666
"@apify/log": "^2.2.6",
67-
"@apify/utilities": "^2.18.0",
67+
"@apify/utilities": "^2.23.2",
6868
"@crawlee/types": "^3.3.0",
6969
"agentkeepalive": "^4.2.1",
7070
"async-retry": "^1.3.3",

rsbuild.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default defineConfig({
3131
externals: {
3232
'node:util': 'node:util',
3333
'node:zlib': 'node:zlib',
34+
crypto: 'node:crypto',
3435
},
3536
},
3637
tools: {

src/resource_clients/dataset.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ow from 'ow';
22

33
import type { STORAGE_GENERAL_ACCESS } from '@apify/consts';
4-
import { createStorageContentSignature } from '@apify/utilities';
4+
import { createStorageContentSignatureAsync } from '@apify/utilities';
55

66
import type { ApifyApiError } from '../apify_api_error';
77
import type { ApiClientSubResourceOptions } from '../base/api_client';
@@ -204,7 +204,7 @@ export class DatasetClient<
204204
let createdItemsPublicUrl = new URL(this._publicUrl('items'));
205205

206206
if (dataset?.urlSigningSecretKey) {
207-
const signature = createStorageContentSignature({
207+
const signature = await createStorageContentSignatureAsync({
208208
resourceId: dataset.id,
209209
urlSigningSecretKey: dataset.urlSigningSecretKey,
210210
expiresInMillis: expiresInSecs ? expiresInSecs * 1000 : undefined,

src/resource_clients/key_value_store.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { JsonValue } from 'type-fest';
55

66
import type { STORAGE_GENERAL_ACCESS } from '@apify/consts';
77
import log from '@apify/log';
8-
import { createHmacSignature, createStorageContentSignature } from '@apify/utilities';
8+
import { createHmacSignatureAsync, createStorageContentSignatureAsync } from '@apify/utilities';
99

1010
import type { ApifyApiError } from '../apify_api_error';
1111
import type { ApiClientSubResourceOptions } from '../base/api_client';
@@ -100,7 +100,7 @@ export class KeyValueStoreClient extends ResourceClient {
100100
const recordPublicUrl = new URL(this._publicUrl(`records/${key}`));
101101

102102
if (store?.urlSigningSecretKey) {
103-
const signature = createHmacSignature(store.urlSigningSecretKey, key);
103+
const signature = await createHmacSignatureAsync(store.urlSigningSecretKey, key);
104104
recordPublicUrl.searchParams.append('signature', signature);
105105
}
106106

@@ -138,7 +138,7 @@ export class KeyValueStoreClient extends ResourceClient {
138138
let createdPublicKeysUrl = new URL(this._publicUrl('keys'));
139139

140140
if (store?.urlSigningSecretKey) {
141-
const signature = createStorageContentSignature({
141+
const signature = await createStorageContentSignatureAsync({
142142
resourceId: store.id,
143143
urlSigningSecretKey: store.urlSigningSecretKey,
144144
expiresInMillis: expiresInSecs ? expiresInSecs * 1000 : undefined,

test/_helper.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ class Browser {
1010
return this.browser;
1111
}
1212

13-
async getInjectedPage(baseUrl, DEFAULT_OPTIONS) {
13+
async getInjectedPage(baseUrl, DEFAULT_OPTIONS, gotoUrl = null) {
1414
const page = await this.browser.newPage();
15+
if (gotoUrl) await page.goto(gotoUrl);
16+
1517
await puppeteerUtils.injectFile(page, `${__dirname}/../dist/bundle.js`);
1618

1719
page.on('console', (msg) => console.log(msg.text()));
@@ -26,6 +28,7 @@ class Browser {
2628
baseUrl,
2729
DEFAULT_OPTIONS,
2830
);
31+
2932
return page;
3033
}
3134

test/datasets.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ describe('Dataset methods', () => {
1919
let client;
2020
let page;
2121
beforeEach(async () => {
22-
page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS);
22+
// Navigate to localhost address to ensure secure context e.g. for Web Crypto API
23+
page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS, baseUrl);
2324
client = new ApifyClient({
2425
baseUrl,
2526
maxRetries: 0,
@@ -406,6 +407,9 @@ describe('Dataset methods', () => {
406407
const url = new URL(res);
407408
expect(url.searchParams.get('signature')).toBeDefined();
408409
expect(url.pathname).toBe(`/v2/datasets/${datasetId}/items`);
410+
411+
const browserRes = await page.evaluate((id) => client.dataset(id).createItemsPublicUrl(), datasetId);
412+
expect(browserRes).toEqual(res);
409413
});
410414

411415
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', () => {
415419
const url = new URL(res);
416420
expect(url.searchParams.get('signature')).toBeNull();
417421
expect(url.pathname).toBe(`/v2/datasets/${datasetId}/items`);
422+
423+
const browserRes = await page.evaluate((id) => client.dataset(id).createItemsPublicUrl(), datasetId);
424+
expect(browserRes).toEqual(res);
418425
});
419426

420427
it('includes provided options (e.g., limit and prefix) as query parameters', async () => {

test/key_value_stores.test.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ describe('Key-Value Store methods', () => {
2121
let client;
2222
let page;
2323
beforeEach(async () => {
24-
page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS);
24+
// Navigate to localhost address to ensure secure context e.g. for Web Crypto API
25+
page = await browser.getInjectedPage(baseUrl, DEFAULT_OPTIONS, baseUrl);
2526
client = new ApifyClient({
2627
baseUrl,
2728
maxRetries: 0,
@@ -619,6 +620,12 @@ describe('Key-Value Store methods', () => {
619620
const url = new URL(res);
620621
expect(url.searchParams.get('signature')).toBeDefined();
621622
expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/records/${key}`);
623+
624+
const browserRes = await page.evaluate(
625+
({ storeId, key }) => client.keyValueStore(storeId).getRecordPublicUrl(key),
626+
{ storeId, key },
627+
);
628+
expect(browserRes).toEqual(res);
622629
});
623630

624631
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', () => {
629636
const url = new URL(res);
630637
expect(url.searchParams.get('signature')).toBeNull();
631638
expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/records/${key}`);
639+
640+
const browserRes = await page.evaluate(
641+
({ storeId, key }) => client.keyValueStore(storeId).getRecordPublicUrl(key),
642+
{ storeId, key },
643+
);
644+
expect(browserRes).toEqual(res);
632645
});
633646
});
634647

@@ -659,6 +672,9 @@ describe('Key-Value Store methods', () => {
659672
const url = new URL(res);
660673
expect(url.searchParams.get('signature')).toBeDefined();
661674
expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/keys`);
675+
676+
const browserRes = await page.evaluate((id) => client.keyValueStore(id).createKeysPublicUrl(), storeId);
677+
expect(browserRes).toEqual(res);
662678
});
663679

664680
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', () => {
668684
const url = new URL(res);
669685
expect(url.searchParams.get('signature')).toBeNull();
670686
expect(url.pathname).toBe(`/v2/key-value-stores/${storeId}/keys`);
687+
688+
const browserRes = await page.evaluate((id) => client.keyValueStore(id).createKeysPublicUrl(), storeId);
689+
expect(browserRes).toEqual(res);
671690
});
672691

673692
it('includes provided options (e.g., limit and prefix) as query parameters', async () => {

0 commit comments

Comments
 (0)