Skip to content

fix(deps): update dependency files-sdk to v3 - #581

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/files-sdk-3.x
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/files-sdk-3.x

Conversation

@renovate

@renovate renovate Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
files-sdk (source) ^2.6.2 → ^3.1.1 age confidence

Release Notes

haydenbleasel/files-sdk (files-sdk)

v3.1.1

Compare Source

Patch Changes
  • cfb6062: Interrupting a buffered DownloadedFile read in files-sdk/effect (text, arrayBuffer, or blob) now cancels the body read underneath, instead of leaving it running after the fiber stops waiting. The read is cancelled only once no other read is waiting on the same body, so a plain StoredFile read in progress is never cut short, and later reads of a cancelled body fail as an aborted (never retried) Provider error. Bodies downloaded through the usage() plugin stay cancellable. Resolves #​173.
  • 0c51802: A plugin failure after a conditional exact read (download(key, { condition: { etag } })) is no longer reported as an applied mutation (#​176). The conditional dispatcher marked any rejection that followed a successful native call with applied: true, so a plugin throwing after the read (for example files-sdk/encryption failing to decrypt) reached the caller, onError, and files-sdk/audit as though the object had changed. A read commits nothing, so that error now comes through unmarked. An applied error a plugin raises itself still passes through untouched, and conditional uploads, deletes, and copies are unchanged.
  • 6936be5: files-sdk/memory ETags no longer collide for different bodies (#​177). The adapter hashed bytes with a 31-bit polynomial hash, so trivially related bodies shared an ETag: "Aa" and "BB" both came out as "00000840", and leading zero bytes didn't change the hash at all. Because sync() treats a matching size and ETag as unchanged, it silently skipped a file whose bytes had changed, and the gateway's If-Range check could accept a stale validator. The ETag is now the quoted MD5 of the bytes (computed in pure JS, so the adapter still runs in browsers and edge runtimes), the same value S3 returns for a single-part upload. Identical content still keeps the same ETag across re-uploads and copies.

v3.1.0

Compare Source

Minor Changes
  • d02e8ca: Add an Effect v4 bridge (files-sdk/effect). Files is a Context.Service provided by Files.layer(), from a Files instance or the options to build one. Its methods mirror Files and return Effects, with listAll, search, and download bodies as Streams. Every operation fails with a FilesError whose reason is a tagged error for the SDK's code (NotFound, Unauthorized, Conflict, ReadOnly, Invalid, Unsupported, Provider), so Effect.catchReason("FilesError", "NotFound", …) recovers from one code, and each reason keeps the SDK error's flags and the original error as its cause. Each call receives its fiber's AbortSignal, so interrupting the fiber (a timeout, a lost race, a closed scope) aborts the provider request. With the events() plugin, files.events.on() runs an Effect handler for each storage event and keeps delivery at-least-once, and files.events.stream() serves the events as a Stream. tryPromise runs plugin methods with the same error mapping and cancellation, and make() provides the service under your own key for multiple buckets or typed plugin methods. effect is an optional peer dependency.
Patch Changes
  • 203ecfe: files-sdk/openai: a needsApproval override in createAgentsFileTools({ overrides }) no longer breaks the tool. It used to replace the Agents SDK's approval function with a boolean, so the runner failed with needsApproval is not a function on the first call. Overrides are now applied when each tool is built, an override takes precedence over requireApproval (and can gate a read tool), and every single-tool factory accepts { description, needsApproval }.
  • 16c1079: Fix files mcp failing to start from the published package. The bundled MCP server read its version from a fixed ../../package.json that only resolved from source, so in an installed package it failed at startup with a misleading "the mcp subcommand requires @modelcontextprotocol/sdk and zod" error even when both were installed. The CLI now finds its own package.json from wherever the build places each module.
  • 203ecfe: The files upload CLI's --metadata flag now takes one key=value pair per flag; repeat the flag for more (--metadata team=finance --metadata quarter=q1). It used to be variadic and swallowed the positional key that followed it, so files upload --metadata a=1 report.pdf --file r.pdf failed. Several pairs after a single --metadata are now refused as extra arguments.
  • 8529ee6: Bulk upload([...]) and download([...]) with stopOnError in files-sdk/client (and the useFiles bindings) now behave like the SDK: items run one at a time in input order, and the call resolves at the first failure with { results, errors: [thatFailure] }. They used to reject while sibling items kept running, so writes could land after the call had failed and isUploading could read false while uploads were still in flight. Items that never started are still reported as "aborted" in the upload state.
  • 8529ee6: download() in files-sdk/client now reports the right size and lastModified more often. On a full proxied download it takes size from the gateway's metadata header instead of Content-Length, which compression middleware can drop (giving 0) or rewrite. A download the gateway redirected to storage now falls back to the storage host's Last-Modified header for lastModified.
  • 8529ee6: files-sdk/client and the useFiles bindings (files-sdk/react, files-sdk/vue, files-sdk/svelte) now reject with a FilesError when a request never gets an answer. A network failure used to surface as the runtime's bare TypeError and a cancelled call as a raw AbortError, and the hooks recorded a hook-level abort() in error as a Provider failure with aborted: false. A network failure is now a Provider FilesError with the original error as its cause, and a cancelled call (per-call signal, hook-level signal, or abort()) is flagged aborted: true, matching the XHR upload path. The hooks rethrow the same error they record in error.
  • 8529ee6: files-sdk/client now classifies error responses that don't come from the gateway by HTTP status, and says why. A body such as Supabase's { "error": "InvalidJWT", "message": "jwt expired" } used to be read as the gateway's envelope and became a Provider error with an empty message, and a 401 from auth middleware in front of the gateway became a generic Provider error. Only a body whose error.code is a string is now treated as the envelope. Anything else maps 401/403 to Unauthorized, 404 to NotFound and 409/412 to Conflict (else Provider), and the message keeps the old gateway responded 401 or upload failed (403) prefix with the body's reason appended when it has one.
  • 8529ee6: A keyless upload() from files-sdk/client (and the useFiles bindings) of a React Native file reference without a size now works. The client used to declare such a file's size as 0, and since the gateway holds an upload to its declared size, the upload was refused. The client now leaves size out of the presign request when it doesn't know it, so the gateway applies only its maxUploadSize.
  • 8529ee6: The reactive useFile, useList and useSearch in files-sdk/react and files-sdk/vue no longer show the previous input's data. Switching to a new key, prefix, pattern, or endpoint used to keep the old result visible while loading (with isLoading false) and next to the new input's error, and a disabled query (useFile(undefined)) kept it too. A new input now starts with no data and isLoading: true, and a disabled query has no data or error. A refetch() of the same input still keeps its data while it reloads, and when the reload fails.
  • 8529ee6: The reactive queries in files-sdk/react, files-sdk/vue and files-sdk/svelte (useFile, useList, useSearch) now honor the signal option. It was documented as merged into every call but was ignored. Aborting it cancels the query's request and settles the query with an aborted FilesError, and the query removes its listener once its request settles or is replaced.
  • 8529ee6: A string body uploaded with files-sdk/client (or the useFiles bindings) and no contentType is now stored as text/plain; charset=utf-8, as the server SDK stores it. It used to be sent untyped and stored as application/octet-stream.
  • 8529ee6: A global Content-Type in the headers option of files-sdk/client (and the useFiles bindings) no longer overrides the type of keyed uploads. It used to replace every upload(key, body) request's type, even an explicit contentType, so files were stored as, say, application/json. The body's own type or contentType now always wins, and the JSON verbs always send application/json.
  • b557950: files-sdk/cloudinary now invalidates the CDN copy when copy() or a resumable (control) upload overwrites an existing asset, as a plain upload() already did. Without it the CDN kept serving the replaced content until its cache expired.
  • b557950: files-sdk/netlify-blobs now rejects keys that @netlify/blobs can't address, with an Invalid error before any request. The SDK puts keys into request URLs unencoded, so upload("Invoice #​42.pdf") stored Invoice , a ? cut the key short in the same way, a \ became /, tabs, newlines and trailing spaces were dropped, and .. segments could even reach another store. Keys containing #, ?, %, \, tabs or newlines, ending in a space or control character, or with . / .. segments now throw instead of silently naming a different blob.
  • b557950: files-sdk/pocketbase now prefers adminEmail / adminPassword passed as options over a POCKETBASE_AUTH_TOKEN environment variable. The env token was checked first, so a stale token left in the environment overrode valid credentials passed in code. The order is now the authToken option, admin credentials from options, POCKETBASE_AUTH_TOKEN, then POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD.
  • b557950: files-sdk/supabase copy() (and so move()) now replaces an existing destination. Supabase only overwrites on copy when asked to, so copying or moving onto a key that already existed failed with a Conflict error, which also broke re-deleting a key with softDelete() and restoring over an existing key. Copies are now sent with x-upsert: true.
  • b557950: files-sdk/supabase bulk deletes of more than 1000 keys now succeed. All keys went out in one request, which Supabase Storage refuses past 1000 objects, so every key was reported as failed. Keys are now deleted in batches of 1000, and a refused batch fails only its own keys.
  • b557950: files-sdk/supabase now percent-encodes keys in every request URL. Keys went into the path unencoded, so a # or ? cut the key short: upload("uploads/Invoice #​42.pdf") stored uploads/Invoice , a later #43 upload silently overwrote it, and head, download and delete disagreed about which object the key named. Uploads, downloads, head, exists, url() (signed and public) and signedUploadUrl() now address exactly the key given, including keys with ?, %, spaces or +. Keys that Supabase Storage itself refuses (such as ones containing # or non-ASCII characters) now fail with Supabase's Invalid key error instead of being truncated.
  • b557950: files-sdk/uploadthing list() now returns only files that finished uploading. It also returned files UploadThing reported as still uploading, failed, or pending deletion, which can't be downloaded. Pagination is unchanged: the cursor still advances past every row UploadThing returned.
  • b557950: files-sdk/vercel-blob copy() now keeps the source's content type and cache max-age. blob.copy doesn't carry them over, so every copy, move and versioning snapshot got a content type guessed from the destination's extension and the default one-month cache. The adapter now reads both from the source and passes them to the copy.
  • b557950: files-sdk/vercel-blob downloads no longer return stale bytes right after an overwrite. The body came through the CDN cache while the size and ETag came from a fresh head(), so for up to a minute the old content was returned labelled with the new metadata. Private downloads now read from origin (useCache: false), and public downloads add a v=<etag> query parameter to the blob URL so each new version misses the cache once.
  • b557950: files-sdk/azure no longer lets environment credentials for one storage account authenticate an adapter configured for another. Previously, azure({ container, accountName: "a" }) picked up an AZURE_STORAGE_CONNECTION_STRING for account b, so data calls went to b and SAS URLs were signed with a's name and b's key (a 403). Now an env connection string whose AccountName differs from an explicit accountName is ignored, and so are AZURE_STORAGE_ACCOUNT_KEY and AZURE_STORAGE_SAS_TOKEN when AZURE_STORAGE_ACCOUNT_NAME (or AZURE_STORAGE_ACCOUNT) names a different account. Env credentials for the same account, or that name no account, still apply.
  • b557950: files-sdk/azure copy() now copies blobs larger than 256 MiB. It used Copy Blob From URL only, which refuses bigger sources with a 409 that surfaced as a Conflict error. Now, when that cap is the cause, it falls back to the asynchronous Copy Blob and resolves once the copy finishes. A failed or aborted copy rejects with a Provider error carrying Azure's reason.
  • b557950: files-sdk/convex upload() no longer fails after the file is stored when reading its metadata back fails. Previously that error escaped unmapped, and the caller never learned the new storage id, which orphaned the file. Now the upload resolves with the new id and the uploaded content type and size.
  • b557950: files-sdk/firebase-storage now prefers a passed app's own storageBucket over FIREBASE_STORAGE_BUCKET. Previously the env var won, so in a process serving several Firebase projects, an adapter built from one project's app used the env var's bucket with that app's credentials. The order is now bucket, then the app's storageBucket, then FIREBASE_STORAGE_BUCKET.
  • b557950: files-sdk/gcs and files-sdk/firebase-storage upload() now take the result's etag, lastModified, and size from the upload response instead of reading the object back afterwards. A service account that can create objects but not read them (Storage Object Creator) no longer gets an Unauthorized error for an upload that landed, and concurrent writers to the same key no longer risk reporting each other's etag. If the response lacks an etag, the follow-up read is best-effort and can't fail the upload.
  • b557950: files-sdk/gcs, files-sdk/firebase-storage, and files-sdk/bunny-storage now honor signal (and timeout) on uploads. Previously an aborted or timed-out upload kept running in the background and could land later, over a newer write. Now aborting cancels the in-flight request: GCS and Firebase destroy the upload stream, and Bunny Storage, whose SDK takes no signal, errors the request body instead. Bunny Storage's download() and copy() honor the signal the same way.
  • db60e17: A conditional upload() in files-sdk whose write committed but whose adapter returned a malformed or weak ETag now rejects with applied: true. Before, the post-commit ETag check failed the call as a plain provider error, hiding that the object had already changed.
  • db60e17: url() and signedUploadUrl() in files-sdk now reject an expiresIn that isn't a positive whole number of seconds with an Invalid error before anything is signed. Before, values like -60, 0, NaN, or 0.5 were passed to the signer and came back as URLs that never worked.
  • db60e17: list(), listAll(), and search() in files-sdk now reject a limit that isn't a positive integer, and search() a negative, fractional, or NaN maxResults, with an Invalid error before any provider call. Before, limit: -1 made the fs, memory, FTP, and SFTP adapters silently drop the last key of each page, 0 or NaN walked nothing, and a bad maxResults returned nothing or everything. maxResults: 0 still yields no matches.
  • db60e17: upload() in files-sdk now rejects a multipart.partSize or multipart.concurrency that isn't a positive integer with an Invalid error before any request, on every upload path (single, bulk, and resumable). Before, a fractional or non-positive value could spin a resumable upload in an endless loop that starved the event loop, silently drop the last byte on Azure, or commit an empty object. The resumable orchestrator also refuses to finalize a part list that doesn't add up to the whole body, and fails a chunk the provider acknowledges without advancing instead of re-sending it forever.
  • db60e17: The files-sdk/ftp, files-sdk/sftp, and files-sdk/webdav adapters now reject keys that contain a backslash or start with a Windows drive letter (C:) with an Invalid error. Before, a key like ..\..\secret passed the .. traversal guard as a single segment, and C:/Windows/win.ini resolved to an absolute path, so on servers running on Windows hosts either could reach files outside the configured root.
  • db60e17: Resumable uploads in files-sdk now open, probe, and finalize their provider session under the same per-attempt timeout, caller signal, control.abort(), and retries as each part. Before, a hung session call ignored the timeout and abort, and a transient provider error there failed the upload despite retries. A session that opens after its attempt timed out or was aborted is discarded as soon as it lands, and a finalize retry that finds the session already gone now reports that the object may have been committed instead of a misleading "not found". Custom resumable drivers receive an optional signal in begin(), probe(), and complete().
  • 21c88cc: files-sdk/box OAuth auth now survives Box's single-use refresh tokens. Before, the rotated refresh token lived only in the instance's memory, so after a restart or in a second instance the configured token was already spent and every call failed with Unauthorized. A new oauth.tokenStorage option persists the rotated tokens (the configured refreshToken becomes the first-run seed), and concurrent refreshes now share one exchange instead of spending the same token several times.
  • 21c88cc: files-sdk/dropbox now strips rootFolderPath from listed keys regardless of case. Dropbox paths are case-insensitive, so with rootFolderPath: "uploads" and a real folder named /Uploads, list() used to return keys like Uploads/a.txt that then addressed /uploads/Uploads/a.txt. Listed keys are now relative to the root (a.txt), as they are when the casing matches.
  • 21c88cc: files-sdk/google-drive no longer trusts a cached file id after another process or the Drive UI deleted, trashed, or replaced the file. Before, head reported NotFound, exists returned false, and delete resolved while the live file stayed. Now, when Drive answers that a cached id is gone, the adapter drops it and looks the key up again once. A trashed file reads as missing, and delete, copy, and url check that a cached id isn't trashed before acting on it.
  • 21c88cc: files-sdk/onedrive and files-sdk/sharepoint now create a missing destination folder before copy(), so copy() and move() into a new folder succeed. Before, Graph's copy failed because its destination folder must already exist, even though upload() to the same key creates intermediate folders, as copy does on Box, Dropbox, and WebDAV.
  • 21c88cc: files-sdk/onedrive and files-sdk/sharepoint no longer reject valid list() cursors from page 2 onward when the drive path is percent-encoded differently in Graph's @odata.nextLink. This affected a siteId (its commas) and folder names containing characters such as @ , ; = + $ &. Cursors are now compared on their decoded paths, and a cursor for another folder is still refused.
  • 21c88cc: files-sdk/dropbox and files-sdk/onedrive (and files-sdk/sharepoint) refresh-token auth now shares one in-flight token exchange. Before, a cold burst of calls sent one token request per call, which risked rate limiting and, with rotating refresh tokens, rejected redemptions. A failed exchange is retried by the next call instead of being replayed.
  • 203ecfe: files-sdk/events: a Google OIDC token whose signature segment isn't base64 is now refused with a 401 ("malformed OIDC token"), before any key fetch. It used to escape as an unexpected error, answered 500 (so Pub/Sub redelivered it) and reported to onError.
  • 203ecfe: files-sdk/events: SNS signature verification now accepts a SigningCertURL only at https://sns.<region>.amazonaws.com/SimpleNotificationService-<id>.pem (or .amazonaws.com.cn). The previous host check also matched S3 bucket endpoints such as sns.s3-accelerate.amazonaws.com, so a bucket named sns could have served a certificate.
  • 203ecfe: files-sdk/events: a webhook with verify: { sns } now parses only the fields of an SNS message that its signature covers. Before, the raw body was parsed after verification, so unsigned EventSource/Sns fields added beside a genuine signed message could feed forged S3 events (a delete, say) to your handlers. The s3 format also refuses a body that is both an SNS notification and a Lambda SNS record.
  • 203ecfe: files-sdk/events no longer reports a deleted event when only one version of an object was removed, since the key may still have a live version. That covers S3 ObjectRemoved:Delete and LifecycleExpiration:Delete carrying a version id (including NoncurrentVersionExpiration cleanups) and EventBridge Permanently Deleted events with a version-id, Backblaze B2 b2:ObjectDeleted:*, and GCS OBJECT_DELETE / Eventarc deleted events whose payload shows the generation was already noncurrent (timeDeleted more than a minute before the event). Delete markers, B2 hide markers, GCS archives and unversioned deletes are still reported, so cleanup handlers no longer delete live data when a lifecycle rule prunes old versions.
  • 51204c2: The files-sdk/api gateway's url operation, and files-sdk/signed-url-policy, now accept a requested responseContentDisposition as an attachment only when its type is exactly attachment. Values such as attachment, inline, attachment inline, or attachment/x passed the old check but render inline in Chromium; they are now replaced with a plain attachment.
  • 51204c2: The files-sdk/api gateway no longer lets a comma-separated content type such as image/png, text/html skip the download sandbox. A browser renders such a value as its last entry, so a proxied download opened inline could run stored HTML as your app; the sandbox now applies to any stored type that isn't exactly one well-formed media type. The gateway also refuses such a content type from a client with a 422 (reason: "type") on the keyed upload PUT, presign, and signed-upload-url, while an absent content type is still accepted.
  • 51204c2: Proxied downloads from the files-sdk/api gateway now name the file in their default Content-Disposition, as attachment; filename="report.pdf" (with an RFC 6266 filename* for a non-ASCII name), taken from the key's last segment. Before, the bare attachment made a browser save a download opened directly from its URL as files. A disposition returned by authorize is still sent exactly as given.
  • 51204c2: The files-sdk/api gateway now answers HEAD on the download route instead of refusing it with 422. A proxied download returns the same headers a GET would (Content-Length, ETag, Last-Modified, the disposition) with no body, without reading the object, and a redirected download returns the same redirect.
  • 51204c2: A full (200) proxied download from the files-sdk/api gateway now carries the object's size in its X-Files-Meta header, so a client can read the size even when compression middleware drops Content-Length. A 206 range response leaves it out.
  • 51204c2: A zero-byte upload through the files-sdk/api gateway no longer fails with 422 missing request body on Bun, Deno, or Cloudflare Workers. Those runtimes give a PUT with Content-Length: 0 a null body, which the keyed upload and the proxy upload now treat as an empty body.
  • 51204c2: The Node gateway bindings (files-sdk/express, files-sdk/fastify, files-sdk/koa, files-sdk/nitro, and files-sdk/nestjs) now work over HTTP/2. Under Fastify's http2: true or node:http2's compatibility API every request failed with a 500, because the HTTP/2 pseudo-headers (:method, :path, :authority, :scheme) were copied into the Web Request; they are now skipped, and the host is taken from :authority when there is no Host header.
  • 51204c2: The files-sdk/api gateway no longer forwards a storage provider's error message to the client. Messages from an adapter's NotFound, Unauthorized, Conflict, and Provider errors could carry absolute filesystem paths, internal hostnames, or bucket names, so those now reach the client as a fixed message per code (not found, storage provider error, …) with the same code and status, and Provider and Unauthorized errors are passed to onError so the detail still reaches your logs. The SDK's own Invalid, Unsupported, and ReadOnly messages, and any FilesError thrown by your authorize, files factory, or onUploadComplete, are still sent as written.
  • 51204c2: The files-sdk/api gateway now refuses keys inside a plugin's reserved storage however they're spelled. On a case-insensitive store or a Windows filesystem, .TRASH/notes.txt or .trash\notes.txt reached the softDelete() trash (and .VERSIONS/… the version store), so a client allowed to delete but not purge could hard-delete a trashed file; those now get 403 like .trash/notes.txt. A restoreVersion versionId containing a backslash, .., or a NUL byte is now refused with 422, like one containing a slash.
  • 51204c2: A search through the files-sdk/api gateway now always walks storage in pages of maxListLimit keys. The client's limit was used as the page size while maxSearchScan counts keys, so limit: 1 turned one request into up to 10,000 provider list() calls; limit is now ignored by the gateway, and maxResults still caps the matches.
  • 51204c2: A keyless upload through the files-sdk/api gateway is now held to the size the client declared at presign, capped by maxUploadSize. Before, authorize saw the declared size but the upload token allowed anything up to maxUploadSize, so a client could understate the size to pass a per-user quota. The proxy PUT and complete now refuse a larger body, a storage-signed target binds the declared size where the adapter can, and a client that doesn't know a file's size may omit size (that upload is held to maxUploadSize alone). A declared size that isn't a non-negative whole number is a 422.
  • 51204c2: Upload tokens from the files-sdk/api gateway are now bound to the origin (scheme and host) of the request that minted them, as well as its path and query. With a files factory that picks the instance from the host, a token minted on one tenant's host could previously be redeemed by the proxy upload PUT on another's; that request, and complete on the wrong host, now get Unauthorized. The docs now also warn that a factory must select the instance from the URL, not from a cookie or header, because the proxy PUT runs no authorize.
  • 9fdc79f: files-sdk/cache no longer caches a stale read that overlapped a write. A slow download(), head(), or url() miss that was in flight while an upload() (or delete, copy, move) of the same key completed used to store what it had fetched after the write invalidated the key, serving the old value for the full ttl; such a read now returns its result without caching it. Writes also drop the key before they run and after any failure, not only after a success or a Conflict, since a timed-out write may still have landed.
  • 9fdc79f: files-sdk/content-type now classifies a document behind an <?xml prolog by its root element, so an XHTML or SVG document can no longer pass as an arbitrary +xml type. Previously any declared +xml type agreed with an XML sniff, so an XHTML page uploaded as image/x+xml passed onMismatch: "reject" (and an image/* allowlist after it) and ran its scripts when served. An html or svg root (or one in the XHTML or SVG namespace) now needs exactly application/xhtml+xml or image/svg+xml, an XML document never agrees with an image/, audio/, video/, or font/ type other than image/svg+xml, and detectContentType() reports application/xhtml+xml for an XHTML root.
  • 9fdc79f: files-sdk/dedup now reserves its blob store (.dedup by default) for the instance, the way versioning() and softDelete() reserve theirs. Previously a files-sdk/api gateway client could list .dedup/, download another user's content by its hash, or delete or move a blob and break every pointer to it; those requests now get a 403. A move out of the store through the instance is also refused (it would remove the blob), and a backslash spelling such as .dedup\x is recognized as a store key.
  • 9fdc79f: files-sdk/failover no longer fails over an upload that carries a resumable control. An UploadControl drives exactly one upload, so replaying it on a secondary after the primary failed threw This UploadControl has already driven an upload and hid the primary's real error. Such uploads now go to the primary only, like stream uploads, and surface its error.
  • 9fdc79f: files-sdk/validation and files-sdk/content-type no longer accept a declared content type that is really a list, such as image/png;a=b, text/html. Both plugins used to check only the text before the first ; and then store the caller's value verbatim, which a browser renders as its last entry (text/html), so a PNG/HTML polyglot could pass allowedTypes: ["image/*"] or contentType({ onMismatch: "reject" }). A declared type that isn't exactly one well-formed media type is now refused with an Invalid error (by validation() whenever allowedTypes is set, and by contentType() in every mode), and the type either plugin forwards is stored with its type and subtype normalized.
  • 9fdc79f: files-sdk/soft-delete no longer hard-deletes a live file through a trash key that resolves out of the trash. A delete(".trash/../notes.txt"), which a filesystem resolves to notes.txt, used to be treated as a delete inside the trash and destroyed the live file; it's now trashed like any live key. purge() and restoreTrashed() refuse a key that resolves out of the trash, such as ../notes.txt, with an Invalid error.
  • 9fdc79f: files-sdk/soft-delete's purge() is now idempotent on adapters whose delete throws NotFound for a missing key, such as GCS and Firebase. purge(key) with nothing trashed used to reject with NotFound there, and a whole-trash purge() failed when an object was purged concurrently; both now resolve.
  • 9fdc79f: files-sdk/soft-delete now explains a trash collision on hierarchical stores such as files-sdk/fs and SFTP. With a already in the trash, deleting a/b needs a .trash/a/ folder where the trashed file sits, and used to fail with a bare EEXIST … mkdir .trash/a error; it now throws a Conflict that names the trashed entry in the way and says to purge() (or restoreTrashed()) it first. The live object is untouched either way.
  • 9fdc79f: files-sdk/tiering with fallback: true now works over tiers whose delete throws NotFound for a missing key, as GCS and Firebase do. The plugin's cleanup deletes assumed a missing key was a no-op, so every first upload rejected with NotFound after it had landed, a copy or move rejected after landing, and a delete of a key held by the other tier threw before reaching it, leaving the object in place. Those cleanup deletes now ignore NotFound; a delete removes the key from whichever tier holds it, and throws NotFound only when both tiers report the key missing.
  • 9fdc79f: files-sdk/versioning's restoreVersion() now refuses a versionId that is empty or ., or that contains a backslash, .., or a NUL byte, as it already refused one containing / (the files-sdk/api gateway refuses the same ids). On a Windows filesystem a versionId such as ..\..\users\2\secret\<id> restored another key's version (possibly another tenant's) into the caller's key. A write to a key such as .versions/../notes.txt, which a filesystem resolves to notes.txt, is also no longer mistaken for a write inside the version store, so it's snapshotted like the live key it is.
  • 22f8f5d: files-sdk/s3, files-sdk/s3-fetch, and every S3-compatible adapter now refuse user metadata values that aren't printable ASCII, including Latin-1 text such as café, with an Invalid error before any request. Such a value used to go out and fail as SignatureDoesNotMatch, reported as Unauthorized, because the HTTP client sends a Latin-1 character as one byte while SigV4 signs it as two. Encode the value first, for example with encodeURIComponent. The R2 binding stores metadata without HTTP headers, so it still accepts any text.
  • 22f8f5d: files-sdk/bun-s3 now reports bad credentials on list(), upload(), delete(), and copy() as Unauthorized. Bun's S3 errors carry no HTTP status, and SignatureDoesNotMatch, InvalidAccessKeyId, ExpiredToken, and InvalidToken weren't recognized, so a wrong key or secret was a Provider error that retries reissued.
  • 22f8f5d: files-sdk/s3, files-sdk/s3-fetch, every S3-compatible adapter, and files-sdk/bun-s3 now refuse a contentType or cacheControl containing control characters (such as CR/LF) or non-ASCII text with an Invalid error before any request, on uploads, resumable uploads, and signedUploadUrl(). Such a value used to fail as a retried Provider error, or as Unauthorized on the aws-sdk client while the fetch client stored it mangled.
  • 22f8f5d: A download() whose range starts past the end of the object (416 InvalidRange) is no longer retried on files-sdk/s3, files-sdk/s3-fetch, the S3-compatible adapters, files-sdk/bun-s3, or the files-sdk/r2 binding (error code 10039). It stays a Provider error, since it's the provider's answer, but is now marked permanent, so retries doesn't reissue a request that can only fail the same way.
  • 22f8f5d: copy() and move() on files-sdk/s3, files-sdk/s3-fetch, and the S3-compatible adapters now handle sources over 5 GiB. S3's single CopyObject request refuses them, which used to surface as a retried Provider error; the adapter now confirms the size with a HEAD and copies the object server-side with a multipart copy (UploadPartCopy), carrying its content type, cache and disposition headers, and user metadata. On AWS every part is pinned to the source's ETag, so a source overwritten mid-copy fails with Conflict. A conditional copy of such a source throws Unsupported, since it can't stay one atomic request.
  • 22f8f5d: multipart.partSize on files-sdk/s3 and the S3-compatible adapters' aws-sdk client is now rounded into what S3 accepts. A size under 5 MiB is raised to S3's minimum instead of failing every attempt with EntityTooSmall, a size over 5 GiB is lowered to the maximum, and when the body's length is known the size grows until the body fits in S3's 10,000 parts, instead of failing at part 10,001 after about 48.8 GiB had uploaded.
  • 22f8f5d: url() and signedUploadUrl() on files-sdk/s3, files-sdk/s3-fetch, the S3-compatible adapters, files-sdk/bun-s3, and the files-sdk/r2 binding's hybrid signer now throw Invalid for an expiresIn or defaultUrlExpiresIn that isn't a whole number of seconds of at least 1. A zero, negative, fractional, or NaN lifetime used to mint a URL that was dead on arrival; only the seven-day ceiling was checked.
  • 22f8f5d: A resumable (control) upload on files-sdk/s3 and the S3-compatible adapters no longer fails after the object has committed when the follow-up HeadObject is denied or fails (for example, a principal allowed PutObject but not GetObject). The upload now resolves with the summed part sizes and the upload's content type, where it used to reject with Unauthorized and leave a resume that could only hit NoSuchUpload.
  • 22f8f5d: An expired or malformed session token (ExpiredToken, InvalidToken, which S3 returns as HTTP 400) is now Unauthorized on files-sdk/s3, files-sdk/s3-fetch, and the S3-compatible adapters, instead of a Provider error that retries reissued.
  • 21c88cc: files-sdk/fs resumable uploads no longer follow a symlink planted at the <key>.fls-part staging path. Starting an upload used to open that path and truncate whatever the link pointed at, inside the root or not, and completing it then renamed the link into place. Now the upload replaces anything at the staging path and creates the file exclusively, and a chunk or completion that finds a symlink there fails with Conflict instead of writing or reading through it.
  • 21c88cc: files-sdk/fs now rejects keys that end in /, or in a . or .. segment, with Invalid. Path resolution used to drop the trailing part, so upload("dir/") wrote a file named dir and delete("a.txt/") deleted a.txt. A delete("dir/") that used to be a silent no-op now throws Invalid too.
  • 21c88cc: files-sdk/ftp now runs calls on an injected client one at a time. An FTP connection can only run one command at a time, and basic-ftp closes the whole connection when a second command starts, so concurrent calls on a shared client (Promise.all, or the bulk forms of upload, download, and head) used to fail and leave the client closed for good. Calls now queue on the client, a failed call no longer blocks the ones behind it, and a streamed download holds the client until the stream is read to the end or cancelled.
  • 21c88cc: files-sdk/ftp and files-sdk/sftp move() now overwrite an existing destination like every other adapter. A plain rename failed whenever the destination existed: OpenSSH and most SFTP servers answered with a generic failure that was retried and then thrown as Provider, and FTP servers that refuse to rename over a file, such as IIS, reported a misleading NotFound. When the server refuses, the destination is now deleted and the rename retried (so the key is briefly absent), except for a case-only rename, which could otherwise delete the source on a case-insensitive server.
  • 21c88cc: files-sdk/webdav streamed downloads (as: "stream") no longer report size: 0 when the server sends a chunked response without a Content-Length header. The adapter now takes the size from a PROPFIND, narrowed to the requested slice for a ranged read.
  • 21c88cc: files-sdk/zip unzip() now decodes entry names the way the archive declares them. Names were always read as UTF-8 with invalid bytes replaced, so archives from Windows Explorer and other tools that write code page 437 produced garbled keys, and two such entries could collapse into one name and fail as duplicates. Names flagged UTF-8, or carried in an Info-ZIP Unicode Path field, are now read as strict UTF-8 and fail closed on invalid bytes. Unflagged names are read as UTF-8 when they are valid UTF-8 and as code page 437 otherwise.

v3.0.0

Compare Source

Major Changes
  • 0223b30: files-sdk/cache no longer shares entries between instances. Store keys now include the instance prefix, and a custom store requires a namespace naming the bucket it caches (cache({ store, namespace: "uploads-prod" })); without one, cache() throws Invalid. Reusing one cache() on an instance with a different adapter or prefix also throws Invalid, so create one per instance. Before, two tenants sharing a store could read each other's cached bytes, URLs, and metadata.

  • 0223b30: The files-sdk/api gateway now refuses client keys and list/search prefixes inside the versioning() store (.versions/) or the softDelete() trash (.trash/) with a 403, so a client can't hard-delete trashed objects, wipe version history, or forge versions through the core operations. Use the plugin operations instead. Under an authorize keyPrefix, a tenant's own folders with those names stay ordinary keys.

  • 0223b30: The Google Drive adapter (files-sdk/google-drive) now refuses upload({ cacheControl }) with Unsupported (capabilities.cacheControl is false): Drive never serves it, and the value was silently dropped. It also declares signedUpload.contentType: true, since the session binds the type, and no longer offers resumable uploads when built from a pre-built client, where they always threw.

  • 0223b30: signedUploadUrl({ minSize }) without maxSize now throws Unsupported on files-sdk/s3, files-sdk/s3-fetch, files-sdk/r2, files-sdk/bun-s3, and the adapters built on them, instead of returning a presigned PUT that silently accepts empty uploads. On s3(), pass maxSize too to get a POST policy that enforces both. minSize: 0 still returns a PUT.

  • 40a3694: The array (bulk) forms of upload, download, head, and delete now resolve to one shape, BulkResult<T> = { results: T[]; errors?: BulkError[] }, instead of four differently named arrays.

    • Field renames: read results where you read uploaded, downloaded, files, or deleted. exists([…]) keeps its { existing, missing, errors? } split.
    • Type changes: UploadManyResult, DownloadManyResult, HeadManyResult, and DeleteManyResult are now aliases of BulkResult. DeleteManyOptions is an alias of BulkOptions, and DeleteManyError is removed in favour of BulkError (the same { key, error } shape).
    • stopOnError on a bulk delete: it now always deletes one key at a time and skips the adapter's native batch, because a batch request can't stop partway. So it returns the same result with or without plugins installed, where before the native path and the plugin path could disagree.
    • Custom adapters: an Adapter.deleteMany returns { results, errors? }.
    • Gateway, client, CLI, and MCP: the files-sdk/api wire, files-sdk/client, the useFiles bindings, and the CLI and MCP bulk output use the same shape.
  • 58e15bc: Adapters now declare what they can do as one capabilities object, and files.capabilities gains the fields the old flags couldn't express.

    • Custom adapters: the separate supportsRange, supportsDelimiter, supportsMetadata, supportsCacheControl, supportsServerSideCopy, reportsUploadProgress, and signedUrl fields on Adapter are replaced by capabilities: { rangeRead, delimiter, metadata, cacheControl, serverSideCopy, uploadProgress, signedUrl, signedUpload }. Every field is optional and defaults to unsupported. resumable and conditional are still derived from resumableUpload and conditional. Declare it per instance, so a constructor option that changes what the adapter can do (a publicBaseUrl, an access mode, the fetch client) is reflected. Rename the old flags: supportsRange → rangeRead, supportsDelimiter: true → delimiter: "any" (or "slash"), supportsMetadata → metadata, supportsCacheControl → cacheControl, supportsServerSideCopy → serverSideCopy, reportsUploadProgress → uploadProgress, top-level signedUrl → capabilities.signedUrl. The old fields are ignored and anything you leave out is off: without signedUrl.supported, url(key, { expiresIn }) throws Unsupported before your url() runs; without signedUpload, the gateway proxies every upload; without signedUrl.disposition or publicUrl, it proxies downloads.
    • files.capabilities: multipart is renamed resumable (it always described upload({ control })). delimiter is now "any" | "slash" | false, since Vercel Blob, Netlify Blobs, Supabase, Dropbox, Box, OneDrive, and SharePoint only accept "/". signedUrl gains expiry: "exact" | "provider" | "none": Box, PocketBase, and Dropbox's 4-hour temporary links are "provider", because the provider sets the lifetime. There's a new signedUpload: { supported, maxSize, contentType, maxExpiresIn? } describing signedUploadUrl(), and plugins that refuse direct uploads (encryption, compression, dedup, validation, contentType) turn it off.
    • Gateway (files-sdk/api): presigning uploads now follows signedUpload instead of the download signedUrl flag. An upload is presigned only when the adapter can bind the gateway's maxUploadSize and the file's content type; otherwise it goes through the proxy, which enforces both. UploadThing in its default public-read mode now reports signedUrl.supported: false (its url() is a permanent link) while still presigning uploads, and Vercel Blob in public mode now gets direct presigned uploads instead of proxied ones.
    • Checks run before plugins: an unsupported range, metadata, cacheControl, control, or delimiter is now refused before any plugin runs, checked against the plugin-narrowed capabilities, so a plugin can no longer do I/O (a version snapshot, a trash move) for a call that's about to be rejected. When a plugin is what turned the option off, the error names it, for example range downloads are not supported by the "encryption" plugin.
  • 7ca626f: FilesError gains two codes, Invalid and Unsupported, split out of Provider, which now only means the backend or transport failed.

    • Invalid: the call itself is wrong. That covers an empty, null-byte, or .. key, a malformed range, ETag, or condition, contradictory options, an expiresIn past a hard cap, missing or bad constructor config, and a file a validation() rule rejects (ValidationError keeps its reason).
    • Unsupported: the call is well-formed but this adapter, in this mode and with these plugins, can't do it. That covers a range, metadata, cacheControl, delimiter, or control the adapter has no primitive for, url() without a publicBaseUrl on FTP/SFTP/WebDAV, a signedUploadUrl() maxSize or contentType the provider can't bind, and the fail-closed refusals of encryption(), compression(), dedup(), validation(), and contentType().
    • permanent: both new codes are always permanent and are never retried or failed over. Provider is still the only retried code. versioning()'s restoreVersion() and softDelete()'s restoreTrashed() now throw NotFound when there's nothing to restore.
    • Code that branches on error.code === "Provider" to catch bad input or unsupported options must also check Invalid / Unsupported.
    • Gateway (files-sdk/api): an Invalid error is answered with a 422 and the wire code Validation (it was a 500), and an Unsupported one with a 422 and the new wire code Unsupported. files-sdk/client and the useFiles bindings turn both back into the matching FilesError code, and map any wire code they don't know to Provider. When presigning an upload, the gateway falls back to its proxy only for an Unsupported or Invalid refusal; a backend failure or an abort now surfaces instead of being masked as a proxy target.
    • CLI: files now exits 5 for a Provider failure, so a script can tell "the backend failed, retry" from "the command is wrong". Invalid, Unsupported, and ReadOnly exit 2, like a usage error. A missing optional @modelcontextprotocol/sdk for files mcp is reported as Unsupported.
  • 9765e54: head(), list(), listAll(), and search() now return a FileInfo ({ key, size, contentType, etag?, lastModified?, metadata? }), metadata with no body, instead of a StoredFile.

    • No more hidden downloads: in v2, head and list results carried text() / arrayBuffer() / blob() / stream() accessors that quietly issued a full download when called. Reading bytes is now always an explicit download(), which still returns a StoredFile. A StoredFile is now a FileInfo plus the File-like name / type aliases and the body accessors.
    • Renamed fields: on head and list results, read contentType where you read type, and key where you read name.
    • Upload results: UploadResult is now the same FileInfo shape.
    • Plugin results: versioning()'s restoreVersion() and softDelete()'s restoreTrashed() resolve to

[!IMPORTANT]
✂ PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@vercel

vercel Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
domainstack Ready Ready Preview Oct 17, 2026 1:50am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — e095a62e Deployed Oct 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants