diff --git a/packages/scripts/package.json b/packages/scripts/package.json index e9d310eb63..02747f2d27 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -22,10 +22,13 @@ "axios": "^1.7.7", "commander": "^12.1.0", "ethers": "^5.7.2", + "file-type": "^19.6.0", + "mime-types": "^2.1.35", "sharp": "0.32.6", "zod": "^3.23.8" }, "devDependencies": { + "@types/mime-types": "^2.1.4", "@types/node": "^22.7.1", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/packages/scripts/src/addOrbitChain/github.ts b/packages/scripts/src/addOrbitChain/github.ts index 82eff72f3e..364769c534 100644 --- a/packages/scripts/src/addOrbitChain/github.ts +++ b/packages/scripts/src/addOrbitChain/github.ts @@ -124,3 +124,31 @@ const createPullRequest = async ( }; export { commitChanges, createBranch, createPullRequest, getContent, getIssue }; + +export const saveImageToGitHub = async ( + branchName: string, + imageSavePath: string, + imageBuffer: Buffer +): Promise => { + try { + // Check if the file already exists in the repository + let sha: string | undefined; + try { + const { data } = await getContent(branchName, imageSavePath); + + if ("sha" in data) { + sha = data.sha; + } + } catch (error) { + // File doesn't exist, which is fine + console.log(`File ${imageSavePath} doesn't exist in the repository yet.`); + } + + // Update or create the file in the repository + await updateContent(branchName, imageSavePath, imageBuffer, sha); + console.log(`Successfully saved image to GitHub at ${imageSavePath}`); + } catch (error) { + console.error("Error saving image to GitHub:", error); + throw error; + } +}; diff --git a/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts b/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts index eee89d6be3..c4853af292 100644 --- a/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts +++ b/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts @@ -26,8 +26,8 @@ Test Chain ### Chain description A test chain. -### Chain logo -https://example.com/logo.png +### Chain logo +https://ipfs.io/ipfs/QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym ### Brand color #FF0000 diff --git a/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap b/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap index fc204e9ebb..5b78e1ebec 100644 --- a/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap +++ b/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap @@ -4,7 +4,7 @@ exports[`Transforms > extractRawChainData > should extract raw chain data from t { "bridge": "0x0000000000000000000000000000000000000002", "chainId": "42161", - "chainLogo": "https://example.com/logo.png", + "chainLogo": "https://ipfs.io/ipfs/QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym", "childCustomGateway": "0x000000000000000000000000000000000000000E", "childErc20Gateway": "0x000000000000000000000000000000000000000F", "childGatewayRouter": "0x0000000000000000000000000000000000000010", diff --git a/packages/scripts/src/addOrbitChain/tests/transforms.test.ts b/packages/scripts/src/addOrbitChain/tests/transforms.test.ts index d0c078f157..5ae7e787cd 100644 --- a/packages/scripts/src/addOrbitChain/tests/transforms.test.ts +++ b/packages/scripts/src/addOrbitChain/tests/transforms.test.ts @@ -1,12 +1,19 @@ -/* eslint-disable jest/no-mocks-import */ - import fs from "fs"; import path from "path"; import sharp from "sharp"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; import { IncomingChainData } from "../schemas"; import { extractRawChainData, + fetchAndProcessImage, nameToSlug, resizeImage, stripWhitespace, @@ -18,6 +25,8 @@ import { mockIncomingChainData, mockOrbitChain, } from "./__mocks__/chainDataMocks"; +import { warning } from "@actions/core"; +import axios from "axios"; describe("Transforms", () => { describe("extractRawChainData", () => { @@ -193,4 +202,165 @@ describe("Transforms", () => { expect(resizedAspectRatio).toBeCloseTo(originalAspectRatio, 2); }); }); + describe("Image Download and Processing", () => { + const downloadedImagePath = path.join( + process.cwd(), + "..", + "..", + "arb-token-bridge-ui", + "public", + "images", + "downloaded_chain_logo.png" + ); + + // Clean up downloaded image after tests + // Comment out the following 'after' block if you want to inspect the downloaded image + afterAll(() => { + if (fs.existsSync(downloadedImagePath)) { + fs.unlinkSync(downloadedImagePath); + console.log("Cleaned up downloaded image"); + } + }); + + it("should download, process, and save the chain logo image from fullMockIssue", async () => { + const rawChainData = extractRawChainData(fullMockIssue); + const imageUrl = rawChainData.chainLogo as string; + + expect(imageUrl).toBeTruthy(); + expect(imageUrl.startsWith("https://")).toBe(true); + + const { buffer, fileExtension } = await fetchAndProcessImage(imageUrl); + + expect(buffer).toBeTruthy(); + expect(buffer.length).toBeGreaterThan(0); + expect(fileExtension).toBeTruthy(); + + const fileName = "downloaded_chain_logo"; + const savedImagePath = saveImageLocally(buffer, fileName, fileExtension); + + expect(savedImagePath).toBeTruthy(); + console.log(`Image downloaded and saved to: ${savedImagePath}`); + + const fullSavePath = path.join( + process.cwd(), + "..", + "..", + "arb-token-bridge-ui", + "public", + savedImagePath + ); + expect(fs.existsSync(fullSavePath)).toBe(true); + + const stats = fs.statSync(fullSavePath); + expect(stats.size).toBeGreaterThan(0); + }); + + it("should download, process, and save the chain logo image from IPFS URL", async () => { + const ipfsUrl = "ipfs://QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym"; + const { buffer, fileExtension } = await fetchAndProcessImage(ipfsUrl); + + expect(buffer).toBeTruthy(); + expect(buffer.length).toBeGreaterThan(0); + expect(fileExtension).toBe(".png"); + }); + + it("should throw an error if the image fetch fails", async () => { + const invalidUrl = "https://example.com/nonexistent-image.png"; + await expect(fetchAndProcessImage(invalidUrl)).rejects.toThrow(); + }); + + it("should handle IPFS gateway URLs correctly", async () => { + const ipfsGatewayUrl = + "https://ipfs.io/ipfs/QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym"; + const { buffer, fileExtension } = await fetchAndProcessImage( + ipfsGatewayUrl + ); + + expect(buffer).toBeTruthy(); + expect(buffer.length).toBeGreaterThan(0); + expect(fileExtension).toBeTruthy(); + + // Verify the image can be processed by sharp + const metadata = await sharp(buffer).metadata(); + expect(metadata.format).toBeTruthy(); + expect(metadata.width).toBeGreaterThan(0); + expect(metadata.height).toBeGreaterThan(0); + }); + + it("should convert IPFS protocol URLs to gateway URLs", async () => { + const ipfsProtocolUrl = + "ipfs://QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym"; + const { buffer, fileExtension } = await fetchAndProcessImage( + ipfsProtocolUrl + ); + + expect(buffer).toBeTruthy(); + expect(buffer.length).toBeGreaterThan(0); + expect(fileExtension).toBeTruthy(); + + // Verify the image can be processed by sharp + const metadata = await sharp(buffer).metadata(); + expect(metadata.format).toBeTruthy(); + expect(metadata.width).toBeGreaterThan(0); + expect(metadata.height).toBeGreaterThan(0); + }); + + it("should handle invalid IPFS hashes gracefully", async () => { + const invalidIpfsUrl = "ipfs://InvalidHash123"; + await expect(fetchAndProcessImage(invalidIpfsUrl)).rejects.toThrow(); + }); + + it("should handle non-image buffers and convert to webp", async () => { + // Create a mock non-image buffer + const nonImageBuffer = Buffer.from("not an image"); + + // Mock axios to return our non-image buffer + const mockUrl = "https://example.com/unknown-file"; + vi.spyOn(axios, "get").mockResolvedValueOnce({ + status: 200, + data: nonImageBuffer, + headers: { + "content-type": "application/octet-stream", + }, + }); + + const { buffer, fileExtension } = await fetchAndProcessImage(mockUrl); + + expect(buffer).toBeTruthy(); + expect(buffer.length).toBeGreaterThan(0); + expect(fileExtension).toBe(".webp"); + }); + }); }); + +const saveImageLocally = ( + imageBuffer: Buffer, + fileName: string, + fileExtension: string +): string => { + const imageSavePath = `images/${fileName}${fileExtension}`; + const fullSavePath = path.join( + process.cwd(), + "..", + "..", + "arb-token-bridge-ui", + "public", + imageSavePath + ); + + // Create directories if they don't exist + const dirs = path.dirname(fullSavePath); + if (!fs.existsSync(dirs)) { + fs.mkdirSync(dirs, { recursive: true }); + } + + if (fs.existsSync(fullSavePath)) { + warning( + `${fileName} already exists at '${imageSavePath}'. Overwriting the existing image.` + ); + } + + fs.writeFileSync(fullSavePath, imageBuffer); + + return `/${imageSavePath}`; +}; diff --git a/packages/scripts/src/addOrbitChain/transforms.ts b/packages/scripts/src/addOrbitChain/transforms.ts index 9543fdb164..9387edd72d 100644 --- a/packages/scripts/src/addOrbitChain/transforms.ts +++ b/packages/scripts/src/addOrbitChain/transforms.ts @@ -6,17 +6,19 @@ import { warning } from "@actions/core"; import { getArbitrumNetworkInformationFromRollup } from "@arbitrum/sdk"; import { JsonRpcProvider } from "@ethersproject/providers"; import axios from "axios"; +import { fileTypeFromBuffer } from "file-type"; import * as fs from "fs"; +import path from "path"; import sharp from "sharp"; import prettier from "prettier"; +import { lookup } from "mime-types"; import { commitChanges, createBranch, createPullRequest, - getContent, getIssue, - updateContent, + saveImageToGitHub, } from "./github"; import { chainDataLabelToKey, @@ -33,6 +35,11 @@ import { const SUPPORTED_IMAGE_EXTENSIONS = ["png", "svg", "jpg", "jpeg", "webp"]; const MAX_IMAGE_SIZE_KB = 100; +export const getFileExtension = (mimeType: string): string => { + const extension = lookup(mimeType); + return extension ? `.${extension}` : ""; +}; + export const initializeAndFetchData = async (): Promise => { core.startGroup("Initialization and Data Fetching"); console.log("Starting initialization and data fetching..."); @@ -265,49 +272,66 @@ export const resizeImage = async ( return resizedImage; }; -export const fetchAndSaveImage = async ( - urlOrPath: string, - fileName: string, - branchName: string -): Promise => { - const isLocalPath = !urlOrPath.startsWith("http"); - if (isLocalPath) { +export const fetchAndProcessImage = async ( + urlOrPath: string +): Promise<{ buffer: Buffer; fileExtension: string }> => { + let imageBuffer: Buffer; + let fileExtension: string; + + // Check if the URL is an IPFS URL or starts with http/https + if (urlOrPath.startsWith("ipfs://") || urlOrPath.startsWith("http")) { + // Handle remote URLs (including IPFS) + if (urlOrPath.startsWith("ipfs://")) { + urlOrPath = `https://ipfs.io/ipfs/${urlOrPath.slice(7)}`; + } + + console.log("Fetching image from:", urlOrPath); + + const response = await axios.get(urlOrPath, { + responseType: "arraybuffer", + timeout: 10000, // 10 seconds timeout + }); + + if (response.status !== 200) { + throw new Error(`Failed to fetch image. Status: ${response.status}`); + } + + imageBuffer = Buffer.from(response.data); + + // Try to determine file extension from response headers + fileExtension = lookup(response.headers["content-type"] as string) || ""; + if (!fileExtension) { + // If not found in headers, try to determine from the image data + const detectedType = await fileTypeFromBuffer(imageBuffer); + fileExtension = detectedType ? `.${detectedType.ext}` : ""; + } + } else { + // Handle local paths const localPath = `../../arb-token-bridge-ui/public/${urlOrPath}`; if (!fs.existsSync(localPath)) { throw new Error( `Provided local path '${localPath}' did not match any existing images.` ); } - return urlOrPath; + imageBuffer = fs.readFileSync(localPath); + fileExtension = path.extname(localPath); } - const fileExtension = urlOrPath.split(".").pop()?.split("?")[0] || ""; - if (!SUPPORTED_IMAGE_EXTENSIONS.includes(fileExtension)) { - throw new Error( - `Invalid image extension '${fileExtension}'. Expected: ${SUPPORTED_IMAGE_EXTENSIONS.join( - ", " - )}.` - ); + // If still not found, default to .webp + if (!fileExtension) { + console.warn("Could not determine file type, defaulting to .webp"); + fileExtension = ".webp"; } - const imageSavePath = `images/${fileName}.${fileExtension}`; - const fullSavePath = `../../arb-token-bridge-ui/public/${imageSavePath}`; - - // Create directories if they don't exist - const dirs = fullSavePath.split("/").slice(0, -1).join("/"); - if (!fs.existsSync(dirs)) { - fs.mkdirSync(dirs, { recursive: true }); - } - - if (fs.existsSync(fullSavePath)) { - warning( - `${fileName} already exists at '${imageSavePath}'. Using the existing image.` + if (!SUPPORTED_IMAGE_EXTENSIONS.includes(fileExtension.replace(".", ""))) { + console.warn( + `Unsupported image extension '${fileExtension}'. Converting to WEBP.` ); - return `/${imageSavePath}`; - } - const response = await axios.get(urlOrPath, { responseType: "arraybuffer" }); - let imageBuffer = Buffer.from(response.data); + // Convert the image to .webp using sharp + imageBuffer = await sharp(imageBuffer).webp().toBuffer(); + fileExtension = ".webp"; + } // Resize the image try { @@ -320,20 +344,17 @@ export const fetchAndSaveImage = async ( } } - // Check if the file already exists in the repository - let sha: string | undefined; - try { - const { data } = await getContent(branchName, imageSavePath); - - if ("sha" in data) { - sha = data.sha; - } - } catch (error) { - // File doesn't exist, which is fine - } - - await updateContent(branchName, imageSavePath, imageBuffer, sha); + return { buffer: imageBuffer, fileExtension }; +}; +export const fetchAndSaveImage = async ( + urlOrPath: string, + fileName: string, + branchName: string +): Promise => { + const { buffer, fileExtension } = await fetchAndProcessImage(urlOrPath); + const imageSavePath = `images/${fileName}${fileExtension}`; + await saveImageToGitHub(branchName, imageSavePath, buffer); return `/${imageSavePath}`; }; diff --git a/yarn.lock b/yarn.lock index 76b538b31e..b8d74abdac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2230,6 +2230,11 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@sec-ant/readable-stream@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" + integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== + "@sentry-internal/browser-utils@8.33.1": version "8.33.1" resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.33.1.tgz#5364532dc4d4a87698b191910a7f1c7d5361da52" @@ -2666,6 +2671,11 @@ dependencies: tippy.js "^6.3.1" +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -2900,6 +2910,11 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz" integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== +"@types/mime-types@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" + integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== + "@types/mime@*": version "3.0.1" resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz" @@ -7549,6 +7564,16 @@ file-type@^11.1.0: resolved "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz" integrity sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g== +file-type@^19.6.0: + version "19.6.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-19.6.0.tgz#b43d8870453363891884cf5e79bb3e4464f2efd3" + integrity sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ== + dependencies: + get-stream "^9.0.1" + strtok3 "^9.0.1" + token-types "^6.0.0" + uint8array-extras "^1.3.0" + file-type@^3.8.0: version "3.9.0" resolved "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz" @@ -7972,6 +7997,14 @@ get-stream@^8.0.1: resolved "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-stream@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27" + integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA== + dependencies: + "@sec-ant/readable-stream" "^0.4.1" + is-stream "^4.0.1" + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" @@ -8934,6 +8967,11 @@ is-stream@^3.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-stream@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b" + integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== + is-string-and-not-blank@^0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/is-string-and-not-blank/-/is-string-and-not-blank-0.0.2.tgz" @@ -10276,7 +10314,7 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -11187,6 +11225,11 @@ pause-stream@0.0.11, pause-stream@^0.0.11: dependencies: through "~2.3" +peek-readable@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.3.1.tgz#9cc2c275cceda9f3d07a988f4f664c2080387dff" + integrity sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw== + pend@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" @@ -13055,6 +13098,14 @@ strip-outer@^1.0.0, strip-outer@^1.0.1: dependencies: escape-string-regexp "^1.0.2" +strtok3@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-9.0.1.tgz#7e3d7bbd2b829c9def6a7bb90d82e240abdd32be" + integrity sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^5.3.1" + styled-components@^5.3.5: version "5.3.11" resolved "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz" @@ -13438,6 +13489,14 @@ toidentifier@1.0.1: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +token-types@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-6.0.0.tgz#1ab26be1ef9c434853500c071acfe5c8dd6544a3" + integrity sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA== + dependencies: + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" + tough-cookie@^4.1.2: version "4.1.3" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz" @@ -13680,6 +13739,11 @@ ufo@^1.4.0, ufo@^1.5.3: resolved "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz" integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== +uint8array-extras@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.4.0.tgz#e42a678a6dd335ec2d21661333ed42f44ae7cc74" + integrity sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ== + uint8arrays@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2"