From 633587bd6696309cd1dd9ef723c1c1222b0f976a Mon Sep 17 00:00:00 2001 From: Alan Bernstein Date: Wed, 7 Oct 2026 10:26:12 +0200 Subject: [PATCH] feat!: upgrade otplib to 13 and require Node 24 otplib 13 removes the authenticator export. login() now uses generateSync(). Its dependency chain is ESM-only, so the CommonJS build needs Node 24. An empty or malformed shared secret now rejects with an Error instead of sending an unlock request. --- package-lock.json | 126 ++++++++++++++++++++++++++---------------- package.json | 6 +- src/domrobot.ts | 4 +- test/domrobot.test.js | 16 +++--- 4 files changed, 91 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1452d9..8e157a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "domrobot-client", - "version": "3.3.1", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "domrobot-client", - "version": "3.3.1", + "version": "4.0.0", "dependencies": { - "otplib": "^12.0.1" + "otplib": "^13.5.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -19,7 +19,7 @@ "typescript-eslint": "^8.71.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@cacheable/memory": { @@ -264,54 +264,81 @@ "dev": true, "license": "MIT" }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@otplib/core": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", - "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.5.0.tgz", + "integrity": "sha512-2rURdkYkb3BDhMs3j/oCCPTve1ybJ6ruLfLfSe1ZSPV+y6RFTbLfAuT0m0ZCnps88ogkIq9t/+Li/kg5RofQwQ==", "license": "MIT" }, - "node_modules/@otplib/plugin-crypto": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", - "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", - "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "node_modules/@otplib/hotp": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.5.0.tgz", + "integrity": "sha512-1EwwAti05CeWJn4xXOVMBK9N6dIJlJw/cQkQyiL9OVlkeS452wAve3ffodi+5IUZdWoF1wxVug38RisdxDqDWw==", + "license": "MIT", + "dependencies": { + "@otplib/core": "13.5.0", + "@otplib/uri": "13.5.0" + } + }, + "node_modules/@otplib/plugin-base32-scure": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.5.0.tgz", + "integrity": "sha512-3JEIHindUMiIeNL0jXepSAkZ/7IkWq4sPdG9eXK0lrMXZG9RdKu5oz/4tuuTodtsFUPYhKlRMqv5xUvQIkMIaA==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1" + "@otplib/core": "13.5.0", + "@scure/base": "^2.2.0" } }, - "node_modules/@otplib/plugin-thirty-two": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", - "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", - "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "node_modules/@otplib/plugin-crypto-noble": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.5.0.tgz", + "integrity": "sha512-fihOAGFvc4b8XTKyIK3jifFP2nLrUNc2bOaJ3UkUKJjy1XI2FbHKG0WmOH+jFsPfa6VsOnouCnEmSznDxIe0pA==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "thirty-two": "^1.0.2" + "@noble/hashes": "^2.2.0", + "@otplib/core": "13.5.0" } }, - "node_modules/@otplib/preset-default": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", - "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", - "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "node_modules/@otplib/totp": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.5.0.tgz", + "integrity": "sha512-GD9LzQnbHDwXCp79s8AaiA5cJR6luX5fEr9AN32AWx4ooRvllPbMkG80gHSA0jbGCnAIX/ChyOtTZ8tgJHMkSg==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "@otplib/plugin-crypto": "^12.0.1", - "@otplib/plugin-thirty-two": "^12.0.1" + "@otplib/core": "13.5.0", + "@otplib/hotp": "13.5.0", + "@otplib/uri": "13.5.0" } }, - "node_modules/@otplib/preset-v11": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", - "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "node_modules/@otplib/uri": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.5.0.tgz", + "integrity": "sha512-LsL1hqTEgJHY40U2eb/Qp6OR1tKkNGwHQTrqqjIuJj0cawGWkLEmeUFK1MHzWBQ4bgy5IdeB5Et7gIDSAH/CRw==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "@otplib/plugin-crypto": "^12.0.1", - "@otplib/plugin-thirty-two": "^12.0.1" + "@otplib/core": "13.5.0" + } + }, + "node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@types/esrecurse": { @@ -390,6 +417,7 @@ "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.71.0", "@typescript-eslint/types": "8.71.0", @@ -581,6 +609,7 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -711,6 +740,7 @@ "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -1048,6 +1078,7 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -1131,14 +1162,17 @@ } }, "node_modules/otplib": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", - "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-13.5.0.tgz", + "integrity": "sha512-RpcC6aq4rANX6MverMuU3pqHVLgMPO7vl6qR8ga7YzoEHlPJknJ58cSVAaf3jBYnasiQVQHXXl2mj/4pYlsVEQ==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "@otplib/preset-default": "^12.0.1", - "@otplib/preset-v11": "^12.0.1" + "@otplib/core": "13.5.0", + "@otplib/hotp": "13.5.0", + "@otplib/plugin-base32-scure": "13.5.0", + "@otplib/plugin-crypto-noble": "13.5.0", + "@otplib/totp": "13.5.0", + "@otplib/uri": "13.5.0" } }, "node_modules/p-limit": { @@ -1199,6 +1233,7 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1298,14 +1333,6 @@ "node": ">=8" } }, - "node_modules/thirty-two": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", - "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", - "engines": { - "node": ">=0.2.6" - } - }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -1355,6 +1382,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index d2985c3..4bf8b38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "domrobot-client", - "version": "3.3.1", + "version": "4.0.0", "description": "INWX Domrobot Node.JS Client", "author": "INWX Developer (https://inwx.com)", "main": "lib/index.js", @@ -33,7 +33,7 @@ "test": "node --test" }, "dependencies": { - "otplib": "^12.0.1" + "otplib": "^13.5.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -44,6 +44,6 @@ "typescript-eslint": "^8.71.0" }, "engines": { - "node": ">=18" + "node": ">=24" } } diff --git a/src/domrobot.ts b/src/domrobot.ts index 6896877..df78ef6 100644 --- a/src/domrobot.ts +++ b/src/domrobot.ts @@ -1,7 +1,7 @@ import * as otplib from 'otplib'; export class ApiClient { - public static readonly CLIENT_VERSION = '3.3.1'; + public static readonly CLIENT_VERSION = '4.0.0'; public static readonly API_URL_LIVE = 'https://api.domrobot.com/jsonrpc/'; public static readonly API_URL_OTE = 'https://api.ote.domrobot.com/jsonrpc/'; @@ -127,7 +127,7 @@ export class ApiClient { if (sharedSecret === null) { return Promise.reject('API requests two factor challenge but no shared secret is given. Aborting.'); } - const secretCode = otplib.authenticator.generate(sharedSecret); + const secretCode = otplib.generateSync({ secret: sharedSecret }); const unlockResult = await this.callApi('account.unlock', { tan: secretCode }); if (unlockResult.code !== 1000) { return unlockResult; diff --git a/test/domrobot.test.js b/test/domrobot.test.js index f494271..ba78f64 100644 --- a/test/domrobot.test.js +++ b/test/domrobot.test.js @@ -7,7 +7,7 @@ const { version } = require('../package.json'); const originalFetch = globalThis.fetch; const TFA_ERROR = 'API requests two factor challenge but no shared secret is given. Aborting.'; -const SHARED_SECRET = 'JBSWY3DPEHPK3PXP'; +const SHARED_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD'; // 32 base32 characters, 20 bytes const SET_COOKIE = 'domrobot=abc; Path=/; HttpOnly'; let calls = []; @@ -260,13 +260,15 @@ describe('login()', () => { }); } - // The behaviour of 3.3.0 and earlier: only null is a missing secret. - test('makes the unlock call for an empty secret and returns its response', async () => { - const unlockData = { code: 2200, msg: 'Authentication error' }; - stubFetch(loginResponse({ code: 1000, resData: { tfa: 'GOOGLE-AUTH' } }), jsonResponse(unlockData)); + // Since 4.0.0: otplib rejects an empty secret, and login() rejects with that error. + test('rejects with an Error and makes no unlock call for an empty secret', async () => { + stubFetch(loginResponse({ code: 1000, resData: { tfa: 'GOOGLE-AUTH' } })); - assert.deepEqual(await new ApiClient().login('user', 'secret', ''), unlockData); - assert.equal(calls[1].body.method, 'account.unlock'); + await assert.rejects(new ApiClient().login('user', 'secret', ''), (reason) => { + assert.ok(reason instanceof Error); + return true; + }); + assert.equal(calls.length, 1); }); });