From ec12a7307fd5aba0fd9546871f902ee197dcc481 Mon Sep 17 00:00:00 2001 From: iammatthias Date: Mon, 27 Apr 2026 16:46:17 -0700 Subject: [PATCH 1/3] fix: honor options.url in uploadFileArray for signed-URL directory uploads uploadFileArray was the only upload function missing an options.url branch, so `pinata.upload.public.fileArray(files).url(signedUrl)` always POSTed to the legacy pinFileToIPFS endpoint with a Bearer token, yielding 401 INVALID_CREDENTIALS in browser flows that construct the SDK with an empty JWT. This adds the missing branch using the v3 directory request shape (top-level name/network/group_id/keyvalues/cid_version/expires_at) and returns res.data, mirroring uploadFile's signed-URL behavior. Also adds SignedUploadUrlOptions.forDirectory, a convenience that injects the synthetic "directory" MIME type into allow_mime_types (merging with caller-supplied mimeTypes). Documents the same requirement on mimeTypes. Test coverage: 23 new tests across tests/uploads/fileArray.test.ts and tests/uploads/createSignedUploadURL.test.ts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../uploads/createSignedUploadURL.ts | 8 +- src/core/functions/uploads/fileArray.ts | 111 +++++- src/core/types/uploads.ts | 21 ++ tests/uploads/createSignedUploadURL.test.ts | 217 +++++++++++ tests/uploads/fileArray.test.ts | 339 ++++++++++++++++++ 5 files changed, 681 insertions(+), 15 deletions(-) create mode 100644 tests/uploads/createSignedUploadURL.test.ts create mode 100644 tests/uploads/fileArray.test.ts diff --git a/src/core/functions/uploads/createSignedUploadURL.ts b/src/core/functions/uploads/createSignedUploadURL.ts index 19da059..e9c88fc 100644 --- a/src/core/functions/uploads/createSignedUploadURL.ts +++ b/src/core/functions/uploads/createSignedUploadURL.ts @@ -58,8 +58,12 @@ export const createSignedUploadURL = async ( payload.max_file_size = options.maxFileSize; } - if (options.mimeTypes) { - payload.allow_mime_types = options.mimeTypes; + if (options.mimeTypes || options.forDirectory) { + const types = new Set(options.mimeTypes ?? []); + if (options.forDirectory) { + types.add("directory"); + } + payload.allow_mime_types = Array.from(types); } let endpoint: string = "https://uploads.pinata.cloud/v3"; diff --git a/src/core/functions/uploads/fileArray.ts b/src/core/functions/uploads/fileArray.ts index 8f1caa3..354706e 100644 --- a/src/core/functions/uploads/fileArray.ts +++ b/src/core/functions/uploads/fileArray.ts @@ -21,6 +21,104 @@ export const uploadFileArray = async ( const folder = options?.metadata?.name || "folder_from_sdk"; + let headers: Record; + + if (config.customHeaders && Object.keys(config.customHeaders).length > 0) { + headers = { + Authorization: `Bearer ${jwt}`, + ...config.customHeaders, + }; + } else { + headers = { + Authorization: `Bearer ${jwt}`, + Source: "sdk/fileArray", + }; + } + + if (options?.url) { + const data = new FormData(); + + for (const file of Array.from(files)) { + const path = file.webkitRelativePath || `${folder}/${file.name}`; + data.append("file", file, path); + } + + data.append("network", network); + data.append("name", folder); + + if (options.groupId) { + data.append("group_id", options.groupId); + } + + if (options.metadata?.keyvalues) { + data.append("keyvalues", JSON.stringify(options.metadata.keyvalues)); + } + + if (options.streamable) { + data.append("streamable", "true"); + } + + if (options.car) { + data.append("car", "true"); + } + + if (options.cid_version !== undefined) { + data.append("cid_version", options.cid_version.toString()); + } + + if (options.expires_at !== undefined) { + data.append("expires_at", options.expires_at.toString()); + } + + try { + const url = new URL(options.url); + + const request = await fetch(url.toString(), { + method: "POST", + headers: headers, + body: data, + }); + + if (!request.ok) { + const errorData = await request.text(); + if (request.status === 401 || request.status === 403) { + throw new AuthenticationError( + `Authentication failed: ${errorData}`, + request.status, + { + error: errorData, + code: "AUTH_ERROR", + metadata: { + requestUrl: request.url, + }, + }, + ); + } + throw new NetworkError(`HTTP error: ${errorData}`, request.status, { + error: errorData, + code: "HTTP_ERROR", + metadata: { + requestUrl: request.url, + }, + }); + } + + const res = await request.json(); + const resData: UploadResponse = res.data; + return resData; + } catch (error) { + if (error instanceof PinataError) { + throw error; + } + if (error instanceof Error) { + throw new PinataError(`Error processing fileArray: ${error.message}`); + } + throw new PinataError( + "An unknown error occurred while uploading an array of files", + ); + } + } + const data = new FormData(); for (const file of Array.from(files)) { @@ -58,19 +156,6 @@ export const uploadFileArray = async ( }), ); - let headers: Record; - - if (config.customHeaders && Object.keys(config.customHeaders).length > 0) { - headers = { - Authorization: `Bearer ${jwt}`, - ...config.customHeaders, - }; - } else { - headers = { - Authorization: `Bearer ${jwt}`, - Source: "sdk/fileArray", - }; - } // Reserved for later release //let endpoint: string = "https://uploads.pinata.cloud/v3"; let endpoint: string = "https://api.pinata.cloud/pinning/pinFileToIPFS"; diff --git a/src/core/types/uploads.ts b/src/core/types/uploads.ts index 7869e4d..ef5626e 100644 --- a/src/core/types/uploads.ts +++ b/src/core/types/uploads.ts @@ -46,7 +46,28 @@ export type SignedUploadUrlOptions = { keyvalues?: Record; vectorize?: boolean; maxFileSize?: number; + /** + * Restrict the signed URL to specific MIME types. + * + * NOTE: For directory uploads (e.g. `pinata.upload.public.fileArray(...).url(...)`), + * this array MUST include the synthetic `"directory"` MIME type — otherwise the + * upload will be rejected with `400 "Presigned URL does not grant permissions to + * upload detected MIME type: directory"`. Aliases like `application/x-directory` or + * `inode/directory` are NOT accepted, and a wildcard (`["*"]` / `["*\/*"]`) does not + * cover it. + * + * Example: `mimeTypes: ["directory"]` (or `["directory", "text/plain"]` to also + * allow plain-text files inside the directory). + */ mimeTypes?: string[]; + /** + * Mint this signed URL for directory uploads (e.g. `fileArray(...).url(...)`). + * + * Adds `"directory"` to `mimeTypes` so the API permits the synthetic directory + * MIME type. Any other entries in `mimeTypes` are preserved, so you can both + * allow a directory and constrain the file types inside it. + */ + forDirectory?: boolean; streamable?: boolean; car?: boolean; /** diff --git a/tests/uploads/createSignedUploadURL.test.ts b/tests/uploads/createSignedUploadURL.test.ts new file mode 100644 index 0000000..ecb4599 --- /dev/null +++ b/tests/uploads/createSignedUploadURL.test.ts @@ -0,0 +1,217 @@ +import { createSignedUploadURL } from "../../src/core/functions"; +import type { PinataConfig, SignedUploadUrlOptions } from "../../src"; +import { + NetworkError, + AuthenticationError, + ValidationError, +} from "../../src/utils/custom-errors"; + +describe("createSignedUploadURL function", () => { + let originalFetch: typeof fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + const mockConfig: PinataConfig = { + pinataJwt: "test-jwt", + pinataGateway: "https://test.mypinata.cloud", + }; + + const signedUrl = + "https://uploads.pinata.cloud/v3/files/00000000-0000-0000-0000-000000000000?X-Algorithm=PINATA1&X-Signature=abc"; + + const getJsonBody = () => { + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + return JSON.parse(fetchCall[1].body as string); + }; + + it("should throw ValidationError if config is missing", async () => { + await expect( + createSignedUploadURL(undefined, { expires: 60 }, "public"), + ).rejects.toThrow(ValidationError); + }); + + it("should POST to /files/sign with the basic payload and return data", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + const url = await createSignedUploadURL( + mockConfig, + { expires: 60 }, + "public", + ); + + expect(url).toBe(signedUrl); + expect(global.fetch).toHaveBeenCalledWith( + "https://uploads.pinata.cloud/v3/files/sign", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: "Bearer test-jwt", + "Content-Type": "application/json", + }), + }), + ); + + const body = getJsonBody(); + expect(body.expires).toBe(60); + expect(body.network).toBe("public"); + }); + + describe("forDirectory option", () => { + it("should add 'directory' to allow_mime_types when forDirectory=true and no mimeTypes set", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + const opts: SignedUploadUrlOptions = { + expires: 60, + forDirectory: true, + }; + await createSignedUploadURL(mockConfig, opts, "public"); + + const body = getJsonBody(); + expect(body.allow_mime_types).toEqual(["directory"]); + }); + + it("should merge 'directory' with caller-supplied mimeTypes", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + const opts: SignedUploadUrlOptions = { + expires: 60, + forDirectory: true, + mimeTypes: ["text/plain", "image/png"], + }; + await createSignedUploadURL(mockConfig, opts, "public"); + + const body = getJsonBody(); + expect(body.allow_mime_types).toEqual( + expect.arrayContaining(["directory", "text/plain", "image/png"]), + ); + expect(body.allow_mime_types).toHaveLength(3); + }); + + it("should not duplicate 'directory' if already in mimeTypes", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + const opts: SignedUploadUrlOptions = { + expires: 60, + forDirectory: true, + mimeTypes: ["directory", "text/plain"], + }; + await createSignedUploadURL(mockConfig, opts, "public"); + + const body = getJsonBody(); + expect(body.allow_mime_types).toEqual(["directory", "text/plain"]); + }); + + it("should not set allow_mime_types when forDirectory is false/absent and no mimeTypes given", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + await createSignedUploadURL(mockConfig, { expires: 60 }, "public"); + + const body = getJsonBody(); + expect(body.allow_mime_types).toBeUndefined(); + }); + + it("should pass mimeTypes through unchanged when forDirectory is not set", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + await createSignedUploadURL( + mockConfig, + { expires: 60, mimeTypes: ["application/json"] }, + "public", + ); + + const body = getJsonBody(); + expect(body.allow_mime_types).toEqual(["application/json"]); + }); + }); + + it("should propagate other options to the request body", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: signedUrl }), + }); + + await createSignedUploadURL( + mockConfig, + { + expires: 120, + groupId: "group-1", + name: "myfile.txt", + keyvalues: { env: "prod" }, + maxFileSize: 1024, + streamable: true, + date: 1700000000, + }, + "private", + ); + + const body = getJsonBody(); + expect(body).toMatchObject({ + expires: 120, + group_id: "group-1", + filename: "myfile.txt", + keyvalues: { env: "prod" }, + max_file_size: 1024, + streamable: true, + date: 1700000000, + network: "private", + }); + }); + + it("should throw AuthenticationError on 401 without retrying", async () => { + const fetchMock = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 401, + url: "https://uploads.pinata.cloud/v3/files/sign", + text: jest.fn().mockResolvedValueOnce("Unauthorized"), + }); + global.fetch = fetchMock; + + await expect( + createSignedUploadURL(mockConfig, { expires: 60 }, "public"), + ).rejects.toThrow(AuthenticationError); + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("should throw NetworkError on 4xx (non-429) without retrying", async () => { + const fetchMock = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 400, + url: "https://uploads.pinata.cloud/v3/files/sign", + text: jest.fn().mockResolvedValueOnce("Bad Request"), + }); + global.fetch = fetchMock; + + await expect( + createSignedUploadURL(mockConfig, { expires: 60 }, "public"), + ).rejects.toThrow(NetworkError); + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/tests/uploads/fileArray.test.ts b/tests/uploads/fileArray.test.ts new file mode 100644 index 0000000..34bca96 --- /dev/null +++ b/tests/uploads/fileArray.test.ts @@ -0,0 +1,339 @@ +import { uploadFileArray } from "../../src/core/functions"; +import type { + PinataConfig, + UploadOptions, + UploadResponse, + CidVersion, +} from "../../src"; +import { + PinataError, + NetworkError, + AuthenticationError, + ValidationError, +} from "../../src/utils/custom-errors"; + +describe("uploadFileArray function", () => { + let originalFetch: typeof fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + const mockConfig: PinataConfig = { + pinataJwt: "test-jwt", + pinataGateway: "https://test.mypinata.cloud", + }; + + const mockFiles = [ + new File(["one"], "a.txt", { type: "text/plain" }), + new File(["two"], "b.txt", { type: "text/plain" }), + ]; + + // Legacy pinFileToIPFS shape (PascalCase keys) + const mockLegacyResponse = { + ID: "legacy-id", + Name: "folder_from_sdk", + IpfsHash: "QmLegacyDirectory", + PinSize: 246, + Timestamp: "2023-01-01T00:00:00Z", + NumberOfFiles: 2, + MimeType: "directory", + GroupId: null, + Keyvalues: {}, + }; + + // v3 envelope-style response (returned by signed-URL endpoint) + const mockV3Response: UploadResponse = { + id: "v3-id", + name: "folder_from_sdk", + cid: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + size: 246, + created_at: "2023-01-01T00:00:00Z", + number_of_files: 2, + mime_type: "directory", + group_id: null, + keyvalues: {}, + vectorized: false, + network: "public", + }; + + it("should throw ValidationError if config is missing", async () => { + await expect( + uploadFileArray(undefined, mockFiles, "public"), + ).rejects.toThrow(ValidationError); + }); + + describe("legacy pinFileToIPFS path", () => { + it("should POST to pinFileToIPFS and remap response", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockLegacyResponse), + }); + + const result = await uploadFileArray(mockConfig, mockFiles, "public"); + + expect(result).toEqual({ + id: "legacy-id", + name: "folder_from_sdk", + cid: "QmLegacyDirectory", + size: 246, + created_at: "2023-01-01T00:00:00Z", + number_of_files: 2, + mime_type: "directory", + group_id: null, + keyvalues: {}, + vectorized: false, + network: "public", + }); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.pinata.cloud/pinning/pinFileToIPFS", + expect.objectContaining({ + method: "POST", + headers: { + Source: "sdk/fileArray", + Authorization: "Bearer test-jwt", + }, + body: expect.any(FormData), + }), + ); + + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + const formData = fetchCall[1].body as FormData; + const fileEntries = formData.getAll("file"); + expect(fileEntries).toHaveLength(2); + + const metadata = JSON.parse(formData.get("pinataMetadata") as string); + expect(metadata.name).toBe("folder_from_sdk"); + + const opts = JSON.parse(formData.get("pinataOptions") as string); + expect(opts.cidVersion).toBe(1); + }); + + it("should respect a custom legacyUploadUrl", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockLegacyResponse), + }); + + await uploadFileArray( + { ...mockConfig, legacyUploadUrl: "https://custom.example/legacy" }, + mockFiles, + "public", + ); + + expect(global.fetch).toHaveBeenCalledWith( + "https://custom.example/legacy", + expect.any(Object), + ); + }); + }); + + describe("signed URL path (options.url)", () => { + const signedUrl = + "https://uploads.pinata.cloud/v3/files/00000000-0000-0000-0000-000000000000?X-Algorithm=PINATA1&X-Signature=abc"; + + it("should POST to options.url instead of pinFileToIPFS", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), + }); + + const result = await uploadFileArray(mockConfig, mockFiles, "public", { + url: signedUrl, + }); + + expect(result).toEqual(mockV3Response); + expect(global.fetch).toHaveBeenCalledTimes(1); + + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + expect(fetchCall[0]).toBe(signedUrl); + expect(fetchCall[1].method).toBe("POST"); + }); + + it("should never hit the legacy pinFileToIPFS endpoint", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), + }); + + await uploadFileArray(mockConfig, mockFiles, "public", { url: signedUrl }); + + const calls = (global.fetch as jest.Mock).mock.calls; + for (const call of calls) { + expect(call[0]).not.toContain("api.pinata.cloud/pinning/pinFileToIPFS"); + } + }); + + it("should send the v3 directory shape, not the legacy pinata* fields", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), + }); + + const options: UploadOptions = { + url: signedUrl, + groupId: "group-1", + metadata: { + name: "my-folder", + keyvalues: { env: "prod" }, + }, + cid_version: "v1" as CidVersion, + expires_at: 1735689600, + streamable: true, + car: true, + }; + + await uploadFileArray(mockConfig, mockFiles, "public", options); + + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + const formData = fetchCall[1].body as FormData; + + expect(formData.get("name")).toBe("my-folder"); + expect(formData.get("network")).toBe("public"); + expect(formData.get("group_id")).toBe("group-1"); + expect(formData.get("keyvalues")).toBe(JSON.stringify({ env: "prod" })); + expect(formData.get("cid_version")).toBe("v1"); + expect(formData.get("expires_at")).toBe("1735689600"); + expect(formData.get("streamable")).toBe("true"); + expect(formData.get("car")).toBe("true"); + + expect(formData.get("pinataMetadata")).toBeNull(); + expect(formData.get("pinataOptions")).toBeNull(); + }); + + it("should preserve webkitRelativePath when present, otherwise prefix with folder name", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), + }); + + const fileWithPath = new File(["x"], "nested.txt", { + type: "text/plain", + }); + Object.defineProperty(fileWithPath, "webkitRelativePath", { + value: "my-folder/sub/nested.txt", + }); + const fileWithoutPath = new File(["y"], "plain.txt", { + type: "text/plain", + }); + + await uploadFileArray( + mockConfig, + [fileWithPath, fileWithoutPath], + "public", + { + url: "https://uploads.pinata.cloud/v3/files/test", + metadata: { name: "the-folder" }, + }, + ); + + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + const formData = fetchCall[1].body as FormData; + const entries = formData.getAll("file") as File[]; + + expect(entries).toHaveLength(2); + expect(entries[0].name).toBe("my-folder/sub/nested.txt"); + expect(entries[1].name).toBe("the-folder/plain.txt"); + }); + + it("should send Bearer header containing the empty JWT for browser/signed-URL flow", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), + }); + + await uploadFileArray( + { pinataJwt: "" }, + mockFiles, + "public", + { url: "https://uploads.pinata.cloud/v3/files/test" }, + ); + + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + expect(fetchCall[1].headers.Authorization).toBe("Bearer "); + }); + + it("should unwrap response.data and not remap legacy fields", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), + }); + + const result = await uploadFileArray(mockConfig, mockFiles, "public", { + url: "https://uploads.pinata.cloud/v3/files/test", + }); + + expect(result.cid).toBe(mockV3Response.cid); + expect(result.mime_type).toBe("directory"); + }); + + it("should throw AuthenticationError on 401", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 401, + url: "https://uploads.pinata.cloud/v3/files/test", + text: jest.fn().mockResolvedValueOnce("token is malformed"), + }); + + await expect( + uploadFileArray(mockConfig, mockFiles, "public", { + url: "https://uploads.pinata.cloud/v3/files/test", + }), + ).rejects.toThrow(AuthenticationError); + }); + + it("should throw NetworkError on non-auth error response", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 400, + url: "https://uploads.pinata.cloud/v3/files/test", + text: jest + .fn() + .mockResolvedValueOnce( + "Presigned URL does not grant permissions to upload detected MIME type: directory", + ), + }); + + await expect( + uploadFileArray(mockConfig, mockFiles, "public", { + url: "https://uploads.pinata.cloud/v3/files/test", + }), + ).rejects.toThrow(NetworkError); + }); + + it("should throw PinataError when fetch itself rejects", async () => { + global.fetch = jest + .fn() + .mockRejectedValueOnce(new Error("Network failure")); + + await expect( + uploadFileArray(mockConfig, mockFiles, "public", { + url: "https://uploads.pinata.cloud/v3/files/test", + }), + ).rejects.toThrow(PinataError); + }); + + it("should pass network=private through to FormData", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ + data: { ...mockV3Response, network: "private" }, + }), + }); + + await uploadFileArray(mockConfig, mockFiles, "private", { + url: "https://uploads.pinata.cloud/v3/files/test", + }); + + const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; + const formData = fetchCall[1].body as FormData; + expect(formData.get("network")).toBe("private"); + }); + }); +}); From 0ae31acebfe63f5fa025ac1769b05bb2ccfda9a5 Mon Sep 17 00:00:00 2001 From: iammatthias Date: Tue, 28 Apr 2026 08:04:59 -0700 Subject: [PATCH 2/3] chore: apply Biome formatting to new test files Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/uploads/createSignedUploadURL.test.ts | 1 - tests/uploads/fileArray.test.ts | 13 ++++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/uploads/createSignedUploadURL.test.ts b/tests/uploads/createSignedUploadURL.test.ts index ecb4599..2163e59 100644 --- a/tests/uploads/createSignedUploadURL.test.ts +++ b/tests/uploads/createSignedUploadURL.test.ts @@ -213,5 +213,4 @@ describe("createSignedUploadURL function", () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); - }); diff --git a/tests/uploads/fileArray.test.ts b/tests/uploads/fileArray.test.ts index 34bca96..f0fb54a 100644 --- a/tests/uploads/fileArray.test.ts +++ b/tests/uploads/fileArray.test.ts @@ -162,7 +162,9 @@ describe("uploadFileArray function", () => { json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), }); - await uploadFileArray(mockConfig, mockFiles, "public", { url: signedUrl }); + await uploadFileArray(mockConfig, mockFiles, "public", { + url: signedUrl, + }); const calls = (global.fetch as jest.Mock).mock.calls; for (const call of calls) { @@ -248,12 +250,9 @@ describe("uploadFileArray function", () => { json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }), }); - await uploadFileArray( - { pinataJwt: "" }, - mockFiles, - "public", - { url: "https://uploads.pinata.cloud/v3/files/test" }, - ); + await uploadFileArray({ pinataJwt: "" }, mockFiles, "public", { + url: "https://uploads.pinata.cloud/v3/files/test", + }); const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; expect(fetchCall[1].headers.Authorization).toBe("Bearer "); From 1e7a16e174ecf4cd1a880a6fbff1132c871465a4 Mon Sep 17 00:00:00 2001 From: iammatthias Date: Wed, 29 Apr 2026 06:57:01 -0700 Subject: [PATCH 3/3] chore: bump version to 2.5.6 Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 755e032..386dac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pinata", - "version": "2.5.5", + "version": "2.5.6", "description": "The official Pinata SDK", "main": "./dist/index.js", "module": "./dist/index.mjs",