diff --git a/package-lock.json b/package-lock.json index 037325d6..d3d42b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "jose": "5.6.3", "jszip": "3.10.1", "multimatch": "6.0.0", - "node-fetch": "3.3.2", "node-notifier": "10.0.1", "open": "9.1.0", "parse-json": "7.1.1", @@ -5025,14 +5024,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", - "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -6155,28 +6146,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6330,17 +6299,6 @@ "node": ">= 14.17" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -8603,41 +8561,6 @@ "type-detect": "4.0.8" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -11321,14 +11244,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -15276,11 +15191,6 @@ "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", "dev": true }, - "data-uri-to-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", - "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" - }, "debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -16094,15 +16004,6 @@ "pend": "~1.2.0" } }, - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -16219,14 +16120,6 @@ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "requires": { - "fetch-blob": "^3.1.2" - } - }, "fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -17853,21 +17746,6 @@ } } }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" - }, - "node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -19814,11 +19692,6 @@ "defaults": "^1.0.3" } }, - "web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 6d12884a..48da7924 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "jose": "5.6.3", "jszip": "3.10.1", "multimatch": "6.0.0", - "node-fetch": "3.3.2", "node-notifier": "10.0.1", "open": "9.1.0", "parse-json": "7.1.1", diff --git a/src/util/submit-addon.js b/src/util/submit-addon.js index fd72d8ab..cfc3c357 100644 --- a/src/util/submit-addon.js +++ b/src/util/submit-addon.js @@ -1,9 +1,8 @@ +import { basename } from 'path'; import { createHash } from 'crypto'; -import { createWriteStream, promises as fsPromises } from 'fs'; +import { createWriteStream, promises as fsPromises, readFileSync } from 'fs'; import { promises as streamPromises } from 'stream'; -// eslint-disable-next-line no-shadow -import fetch, { FormData, fileFromSync } from 'node-fetch'; import { SignJWT } from 'jose'; import JSZip from 'jszip'; import { HttpsProxyAgent } from 'https-proxy-agent'; @@ -15,6 +14,29 @@ const log = createLogger(import.meta.url); export const defaultAsyncFsReadFile = fsPromises.readFile; +// Used by fileFromSync method to make sure the form-data entry will +// include a filename derived from the file path. +// +// TODO: Get rid of this hack when we will bump the web-ext nodejs +// version required to nodejs v20 (where the native File constructor +// exists). +export class FileBlob extends Blob { + #name = ''; + + constructor(fileBits, fileName, options) { + super(fileBits, options); + this.#name = String(fileName); + } + + get name() { + return this.#name; + } + + get [Symbol.toStringTag]() { + return 'File'; + } +} + export class JwtApiAuth { #apiKey; #apiSecret; @@ -88,8 +110,13 @@ export default class Client { this.userAgentString = userAgentString; } - fileFromSync(path) { - return fileFromSync(path); + fileFromSync(filePath) { + // create a File blob from a file path, and ensure it to have the file path basename + // as the associated filename, the AMO server API will be checking it on the form-data + // submitted and fail with the error message: + // "Unsupported file type, please upload a supported file (.crx, .xpi, .zip)." + const fileData = readFileSync(filePath); + return new FileBlob([fileData], basename(filePath)); } nodeFetch(url, { method, headers, body, agent }) { diff --git a/tests/unit/test-util/test.submit-addon.js b/tests/unit/test-util/test.submit-addon.js index 72ec3514..53dff224 100644 --- a/tests/unit/test-util/test.submit-addon.js +++ b/tests/unit/test-util/test.submit-addon.js @@ -8,8 +8,6 @@ import { assert, expect } from 'chai'; import JSZip from 'jszip'; import { afterEach, before, beforeEach, describe, it } from 'mocha'; import * as sinon from 'sinon'; -// eslint-disable-next-line no-shadow -import { File, FormData, Response } from 'node-fetch'; import { AMO_BASE_URL } from '../../../src/program.js'; import Client, { @@ -18,6 +16,9 @@ import Client, { saveIdToFile, saveUploadUuidToFile, signAddon, + + // eslint-disable-next-line no-shadow -- Not actually available under Node 20. Use global once possible. + FileBlob as File, } from '../../../src/util/submit-addon.js'; import { withTempDir } from '../../../src/util/temp-dir.js'; @@ -27,6 +28,19 @@ class JSONResponse extends Response { } } +// Used to test responses with status < 100 (nodejs native constructor +// enforces status to be in the 200-599 range and throws if it is not). +class BadResponse extends Response { + constructor(data, fakeStatus) { + super(data); + this.fakeStatus = fakeStatus; + } + + get status() { + return this.fakeStatus; + } +} + const mockNodeFetch = (nodeFetchStub, url, method, responses) => { // Trust us... You don't want to know why... but if you really do like nightmares // take a look to the details and links kindly provided in this comment @@ -42,6 +56,9 @@ const mockNodeFetch = (nodeFetchStub, url, method, responses) => { for (let i = 0; i < responses.length; i++) { const { body, status } = responses[i]; stubMatcher.onCall(i).callsFake(async () => { + if (status < 200) { + return new BadResponse(body, status); + } if (typeof body === 'string') { return new Response(body, { status }); } @@ -342,6 +359,21 @@ describe('util.submit-addon', () => { assert.equal(client.apiUrl.href, new URL(`${cleanUrl}/addons/`).href); }); + describe('fileFromSync', () => { + it('should return a File with name set to the file path basename', () => + withTempDir(async (tmpDir) => { + const client = new Client(clientDefaults); + const FILE_BASENAME = 'testfile.txt'; + const FILE_CONTENT = 'somecontent'; + const filePath = path.join(tmpDir.path(), FILE_BASENAME); + await fsPromises.writeFile(filePath, FILE_CONTENT); + const fileRes = client.fileFromSync(filePath); + assert.equal(fileRes.name, FILE_BASENAME); + assert.equal(await fileRes.text(), FILE_CONTENT); + assert.equal(String(fileRes), '[object File]'); + })); + }); + describe('getPreviousUuidOrUploadXpi', () => { it('calls doUploadSubmit if previous hash is different to current', async () => { const oldHash = 'some-hash';