diff --git a/.bumpversion.toml b/.bumpversion.toml index 8e748ca..ed27056 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,7 +1,7 @@ # Configuration file for bumpversion # See https://github.com/callowayproject/bump-my-version [tool.bumpversion] -current_version = "1.26.0" +current_version = "1.27.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index d896ef2..e97d54a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [1.27.0] - 2026-04-27 +### Changed +- Replaced the `uuid` dependency with Node's built-in `crypto.randomUUID()`. + Resolves GHSA-w5hq-g745-h8pq (`uuid <14.0.0`) and removes a runtime + dependency. +- Bumped minimum Node version from `>=12.0` to `>=14.17`. Node 12 reached + end of life in April 2022, and `crypto.randomUUID()` is available from + Node 14.17 onward. + +### Fixed +- HTTP requests that fail with `ECONNRESET`, `EPIPE`, or `EAI_AGAIN` are now + retried. Previously these were classified as non-retryable, surfacing + transient transport failures (e.g. stale keep-alive sockets) as hard errors + on the first attempt. Behavior now aligns with deepl-python. + ### Security - Bump follow-redirects to 1.16.0 due to GHSA-r4q5-vmmm-2653. diff --git a/package-lock.json b/package-lock.json index 4b6e316..5a742bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,24 @@ { "name": "deepl-node", - "version": "1.26.0", + "version": "1.27.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "deepl-node", - "version": "1.26.0", + "version": "1.27.0", "license": "MIT", "dependencies": { "@types/node": ">=12.0", "adm-zip": "^0.5.16", "axios": "^1.7.4", "form-data": "^3.0.4", - "loglevel": ">=1.6.2", - "uuid": "^8.3.2" + "loglevel": ">=1.6.2" }, "devDependencies": { "@types/adm-zip": "^0.5.7", "@types/jest": "^29.5.0", "@types/mock-fs": "^4.13.4", - "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "dotenv": "^16.4.7", @@ -73,7 +71,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1296,12 +1293,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -1324,7 +1315,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz", "integrity": "sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.44.0", "@typescript-eslint/type-utils": "5.44.0", @@ -1358,7 +1348,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.44.0.tgz", "integrity": "sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.44.0", "@typescript-eslint/types": "5.44.0", @@ -1513,7 +1502,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1891,7 +1879,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2477,7 +2464,6 @@ "integrity": "sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint/eslintrc": "^1.3.3", "@humanwhocodes/config-array": "^0.11.6", @@ -2662,7 +2648,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", @@ -3867,7 +3852,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4132,6 +4116,16 @@ "node": ">=10.12.0" } }, + "node_modules/jest-junit/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -5938,7 +5932,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6016,14 +6009,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6221,7 +6206,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "requires": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -7128,12 +7112,6 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, "@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -7154,7 +7132,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz", "integrity": "sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.44.0", "@typescript-eslint/type-utils": "5.44.0", @@ -7172,7 +7149,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.44.0.tgz", "integrity": "sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.44.0", "@typescript-eslint/types": "5.44.0", @@ -7253,8 +7229,7 @@ "version": "8.8.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true, - "peer": true + "dev": true }, "acorn-jsx": { "version": "5.3.2", @@ -7518,7 +7493,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7920,7 +7894,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.28.0.tgz", "integrity": "sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==", "dev": true, - "peer": true, "requires": { "@eslint/eslintrc": "^1.3.3", "@humanwhocodes/config-array": "^0.11.6", @@ -8081,7 +8054,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", "dev": true, - "peer": true, "requires": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", @@ -8895,7 +8867,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9075,6 +9046,14 @@ "strip-ansi": "^6.0.1", "uuid": "^8.3.2", "xml": "^1.0.1" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } } }, "jest-leak-detector": { @@ -10317,8 +10296,7 @@ "version": "4.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", - "dev": true, - "peer": true + "dev": true }, "uglify-js": { "version": "3.19.3", @@ -10358,11 +10336,6 @@ "punycode": "^2.1.0" } }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 9a156c5..4e11e48 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "deepl-node", "description": "deepl-node is the official DeepL Node.js client library", - "version": "1.26.0", + "version": "1.27.0", "author": "DeepL SE (https://www.deepl.com)", "license": "MIT", "repository": { @@ -11,7 +11,7 @@ "bugs": "https://github.com/DeepLcom/deepl-node/issues", "homepage": "https://www.deepl.com/", "engines": { - "node": ">=12.0" + "node": ">=14.17" }, "keywords": [ "deepl", @@ -23,14 +23,12 @@ "adm-zip": "^0.5.16", "axios": "^1.7.4", "form-data": "^3.0.4", - "loglevel": ">=1.6.2", - "uuid": "^8.3.2" + "loglevel": ">=1.6.2" }, "devDependencies": { "@types/adm-zip": "^0.5.7", "@types/jest": "^29.5.0", "@types/mock-fs": "^4.13.4", - "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "dotenv": "^16.4.7", diff --git a/src/client.ts b/src/client.ts index 91b4520..d4e0e0c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -15,6 +15,19 @@ import * as http from 'http'; type HttpMethod = 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH'; +/** + * Axios error codes for transient transport-level failures that should be retried. + * Mirrors deepl-python's broader ConnectionError retry coverage. + * @internal + */ +export const RETRYABLE_AXIOS_ERROR_CODES = new Set([ + 'ETIMEDOUT', + 'ECONNABORTED', + 'ECONNRESET', + 'EPIPE', + 'EAI_AGAIN', +]); + const axiosInstance = axios.create({ httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), @@ -261,9 +274,7 @@ export class HttpClient { const error = new ConnectionError(`Connection failure: ${message}`); error.error = axiosError; - if (axiosError.code === 'ETIMEDOUT') { - error.shouldRetry = true; - } else if (axiosError.code === 'ECONNABORTED') { + if (axiosError.code !== undefined && RETRYABLE_AXIOS_ERROR_CODES.has(axiosError.code)) { error.shouldRetry = true; } else { logDebug('Unrecognized axios error', axiosError); diff --git a/src/documentMinifier.ts b/src/documentMinifier.ts index 2cbebf0..e634a06 100644 --- a/src/documentMinifier.ts +++ b/src/documentMinifier.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'crypto'; import { DocumentDeminificationError, DocumentMinificationError } from './errors'; import AdmZip from 'adm-zip'; import { FsHelper } from './fsHelper'; @@ -283,7 +283,7 @@ export class DocumentMinifier implements IDocumentMinifier { * @throws {DocumentMinificationError} If the temporary directory could not be created */ private static createTemporaryDirectory(): string { - const tempDir = path.join(os.tmpdir(), 'document_minification_' + uuidv4()); + const tempDir = path.join(os.tmpdir(), 'document_minification_' + randomUUID()); if (fs.existsSync(tempDir)) { throw new DocumentMinificationError( diff --git a/src/translator.ts b/src/translator.ts index 21a5bce..bd5caaf 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -697,7 +697,7 @@ export class Translator { sendPlatformInfo: boolean, appInfo: AppInfo | undefined, ): string { - let libraryInfoString = 'deepl-node/1.26.0'; + let libraryInfoString = 'deepl-node/1.27.0'; if (sendPlatformInfo) { const systemType = os.type(); const systemVersion = os.version(); diff --git a/tests/client.test.ts b/tests/client.test.ts index b1e4b63..3f8da96 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -5,6 +5,7 @@ import * as deepl from 'deepl-node'; import { exampleText, makeDeeplClient, makeTranslator } from './core'; +import { RETRYABLE_AXIOS_ERROR_CODES } from '../src/client'; import log from 'loglevel'; jest.mock('loglevel', () => ({ @@ -17,6 +18,22 @@ jest.mock('loglevel', () => ({ })); describe('client tests', () => { + describe('retry classification', () => { + it.each([['ETIMEDOUT'], ['ECONNABORTED'], ['ECONNRESET'], ['EPIPE'], ['EAI_AGAIN']])( + 'treats axios error code %s as retryable', + (code) => { + expect(RETRYABLE_AXIOS_ERROR_CODES.has(code)).toBe(true); + }, + ); + + it.each([['ENOTFOUND'], ['ECONNREFUSED'], ['CERT_HAS_EXPIRED']])( + 'does not treat axios error code %s as retryable', + (code) => { + expect(RETRYABLE_AXIOS_ERROR_CODES.has(code)).toBe(false); + }, + ); + }); + describe('log debug', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/tests/core.ts b/tests/core.ts index 8577b78..0838760 100644 --- a/tests/core.ts +++ b/tests/core.ts @@ -7,7 +7,7 @@ import * as deepl from 'deepl-node'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { v4 as randomUUID } from 'uuid'; +import { randomUUID } from 'crypto'; // Note: this constant cannot be exported immediately, because exports are locally undefined const internalExampleText: Record = { diff --git a/tests/documentMinification/helperMethods.test.ts b/tests/documentMinification/helperMethods.test.ts index af2f1f7..49a15c0 100644 --- a/tests/documentMinification/helperMethods.test.ts +++ b/tests/documentMinification/helperMethods.test.ts @@ -5,7 +5,7 @@ import { DocumentMinifier } from '../../src/documentMinifier'; import AdmZip from 'adm-zip'; import { DocumentMinificationError } from '../../src'; import { FsHelper } from '../../src/fsHelper'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'crypto'; import mock from 'mock-fs'; describe('DocumentMinifier helperMethods', () => { @@ -32,7 +32,7 @@ describe('DocumentMinifier helperMethods', () => { }); it('should use provided temp directory when specified', () => { - const customTempDir = path.join(os.tmpdir(), 'custom_temp_dir' + uuidv4()); + const customTempDir = path.join(os.tmpdir(), 'custom_temp_dir' + randomUUID()); fs.mkdirSync(customTempDir); const minifier = new DocumentMinifier(customTempDir); diff --git a/tests/glossary.test.ts b/tests/glossary.test.ts index 8a47616..12187c4 100644 --- a/tests/glossary.test.ts +++ b/tests/glossary.test.ts @@ -6,7 +6,7 @@ import * as deepl from 'deepl-node'; import fs from 'fs'; import { makeTranslator, tempFiles, withRealServer, testTimeout } from './core'; -import { v4 as randomUUID } from 'uuid'; +import { randomUUID } from 'crypto'; const invalidGlossaryId = 'invalid_glossary_id'; const nonExistentGlossaryId = '96ab91fd-e715-41a1-adeb-5d701f84a483'; diff --git a/tests/multilingualGlossary.test.ts b/tests/multilingualGlossary.test.ts index f8194b0..5c1e564 100644 --- a/tests/multilingualGlossary.test.ts +++ b/tests/multilingualGlossary.test.ts @@ -4,7 +4,7 @@ import * as deepl from 'deepl-node'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'crypto'; import { makeDeeplClient, withRealServer } from './core'; describe('Multilingual Glossary Tests', () => { @@ -29,7 +29,7 @@ describe('Multilingual Glossary Tests', () => { constructor(client: deepl.DeepLClient, testName: string) { this.client = client; - const uuid = uuidv4(); + const uuid = randomUUID(); this.glossaryName = `${GLOSSARY_NAME_PREFIX}: ${testName} ${uuid}`; }