Minor Changes
-
4563817: Add provider-native conditional create, replace, exact-read, delete, and copy operations to the existing plugin, hook, retry, prefix, read-only, and receipt pipeline. AWS S3 implements the initial atomic primitives; unsupported adapters, filesystem, custom S3 endpoints, R2, bulk, and multipart paths fail closed.
The S3 adapter exposes the primitives only for canonical AWS — no
endpointand noAWS_ENDPOINT_URL_S3/AWS_ENDPOINT_URLredirect — with a newconditionaloption to override that in either direction, and verifies per request that the resolved hostname is AWS (so a shared-configendpoint_urlfails closed too) and that the installed@aws-sdk/client-s3serialized every predicate header (conditional copy needs 3.919.0+; the@aws-sdk/client-s3peer range moves from^3.700.0to^3.1079.0). The CLI gains--if-match/--if-none-match/--dest-if-matchonupload,download,delete, andcopy, and the MCP tools accept a matchingconditioninput.cache()now invalidates after a failed write too;softDelete()forwards a conditional delete of an already-trashed key;dedup()rejects every conditional mode.A conditional mutation that commits before an awaited plugin rejects the call now surfaces as
FilesError.applied === true(withappliedEtagfor uploads) — on the rejected error, inonError/onAction, and in theaudit()record — so callers can reconcile instead of retrying a predicate that can only conflict. A plugin that re-invokesnext()after the native call failed gets that first failure ascause.rejectConditional(op, plugin, reason)is exported as the one veto shape for plugins with out-of-band side effects. Ordinary operations typemodeasundefined(there never was an"overwrite"value to branch on).
Patch Changes
- 5bc70b1: Fix
azure()buffereddownload()opening a GET whose body was never read or destroyed before issuing the realdownloadToBufferrequest, which held the first response's socket open until garbage collection. The adapter now fetches the metadata with a lightweightgetProperties()call instead; the returned file is unchanged. - 5bc70b1: Fix
azure()list()never returning blobmetadataon its items. Azure only includes metadata in listing responses when explicitly asked, so the adapter now passesincludeMetadata: trueto both flat and hierarchical listings. - 5bc70b1: Fix
azure()resumable uploads throwingNotFoundwhen resuming a session that was paused or persisted before its first block landed. Azure answersGetBlockListwith a 404 when the blob has no committed or uncommitted blocks yet; the adapter now treats that as an empty session and uploads from the start. - 5bc70b1: Fix
azure()copy()andmove()failing withCannotVerifyCopySourcein SAS-token mode. The service client is built with the SAS in its URL and the SDK carries that query through to every blob URL, so the adapter was appending the token a second time (?sv=…?sv=…). The copy source now reuses the SAS already present on the blob URL. - 5bc70b1: Re-uploading a key larger than 50 MB to Box now creates a new version of the existing file instead of failing with
Conflict(item_name_in_use). The Box SDK'suploadBigFilehelper always opens a new-file upload session, so the adapter now opens an existing-file session for the resolved file ID and drives the same part-upload and commit loop itself. - 5bc70b1: Fixed the Bunny Storage adapter returning a directory listing instead of the file's contents when reading the body of an item returned by
list()(orhead()). The lazy body now downloads by the entry's full key rather than relying on the Bunny SDK'sentry.data(), which fetches the entry's containing directory for listing results. - 5bc70b1: Fixed the CLI's
--application-key-idand--application-keyflags being ignored for--provider backblaze-b2, which made the documented invocation fail with "missing credentials". The B2 provider now also lists--regionas required (the adapter has no environment fallback for it), and the provider catalog'sbackblaze-b2entry listsregionin its required config. - 5bc70b1: Fixed the CLI's
existscommand with multiple keys exiting with code 1 ("missing") when a hard error such as an authentication failure occurred alongside a missing key. The hard error's mapped exit code now wins. - 5bc70b1: Fixed the CLI silently dropping the
--endpointflag for--provider r2, so jurisdiction-specific buckets and S3-compatible stand-ins can now be targeted without an account id. - 5bc70b1: The CLI's
search --match <mode>flag now validates its value againstglob,regex,substring, andexact, so a typo is reported as an invalid choice instead of falling through to a confusing regex error. - 5bc70b1: The browser client's XHR transport now rejects immediately when handed an already-aborted
signal. It previously calledxhr.abort()beforesend(), which per the XMLHttpRequest spec fires noabortevent, so the upload promise never settled in real browsers and callers awaiting an upload with a pre-aborted signal hung forever. The rejection is the same abort error an in-flight abort produces, and no request is opened. - 5bc70b1: Bulk
upload([...])no longer silently retries buffered bodies using the client'sretriessetting. It now matches the documented behavior of every other bulk verb: each item is attempted once and a failure lands inerrors. Single-keyupload()keeps its retry budget, andonRetrystill fires only for single-operation calls. - 5bc70b1: A failed
ReadableStreamupload withonProgressno longer leaves the caller's stream locked. The progress-counting wrapper now acquires its reader lazily on first pull, and on failure the SDK cancels the wrapper so the source stream'scancelruns andbody.lockedisfalseafterward. Previously the reader was grabbed eagerly, so an adapter that failed before reading a byte left the caller unable to cancel or reuse their stream. - 5bc70b1: A plugin
wrapthat throws a plainErrornow surfaces to the caller as aFilesErrorregardless of whetheronActionoronErrorhooks are installed. Previously the hook-free fast path let the raw error escape unwrapped, soerror instanceof FilesErrorchecks behaved differently depending on the client's hook configuration. - 5bc70b1:
UploadControl.abort()is now honored when it lands while a resumable upload is finalizing. Previously an abort that arrived after the last chunk but during the provider'scomplete()call was overwritten: the control flipped from "aborted" to "completed" andupload()resolved. The orchestrator now checks for an abort before finalizing and again after, soupload()rejects with the abortedFilesErrorand the control stays aborted. - 5bc70b1:
files.search()now finds keys when the glob contains a backslash escape such asa\*b/x. The literal prefix pushed down to the provider's list call used to keep the escape verbatim (a\*b/x), so nothing was listed even though the matcher correctly accepted the keya*b/x. Escapes are now unwrapped to the literal characters they stand for before the prefix is applied. - 5bc70b1: A caller-supplied
signalnow keeps reaching a lazily-streamed body after the operation call has resolved. Previously, when atimeoutwas configured or a per-call signal was combined with the client's constructor signal, the SDK minted a merged signal and detached it from its sources as soon as the adapter call settled, so aborting the caller's signal no longer interrupted adownload()body still being read. Caller signals are now folded withAbortSignal.any(with a manual fallback on older runtimes) and stay wired for the life of the operation, while only the per-attempt timeout timer is disarmed once the call resolves, so a timeout still never cuts off a body that is streaming after the call succeeded. - 5bc70b1: A
timeoutofInfinity(or any value past the 32-bitsetTimeoutlimit) no longer aborts every operation after about a millisecond with "Operation timed out after Infinityms". Non-finite timeouts now mean "no timeout", and finite values beyond the limit are clamped to it, consistently across single operations, bulk calls, and resumable-upload chunks. - 5bc70b1: Dropbox
url()on a public shared link now rewritesdl=0todl=1correctly on current/scl/fi/...?rlkey=...&dl=0links. The old rewrite matched the literal?dl=0prefix, which no longer comes first on these links, and appended a second parameter, yielding...&dl=0&dl=1and serving the preview page instead of the raw bytes. The parameter is now set through the URL parser regardless of its position. - 5bc70b1: The Dropbox adapter now caps
multipart.partSizeat the 150 MB per-request limit of Dropbox upload sessions (rounded down to the required 4 MiB multiple, so 148 MiB). Previously a largerpartSizewas rounded but never capped, producing session appends the API rejects. - 5bc70b1:
dropbox({ publicByDefault: true })now reuses an existing shared link instead of failing withConflict. The Dropbox SDK stores the whole parsed error body on the thrown error, so theshared_link_already_existsmetadata sits one envelope deeper than the adapter was reading;url()on a key whose public link had already been created therefore never found the existing URL and fell through to the generic conflict error. The adapter now reads the enveloped shape (and still tolerates the bare one). - 5bc70b1: Fixed
failover()secondaries and thetiering()cold tier ignoring the instance's constructor-leveltimeout,retries, andsignal. The internalFileseach plugin builds around its extra adapter was created without those defaults, so after the primary timed out and the chain failed over, a hung secondary (or a hung cold backend) stalled the operation forever instead of timing out. Those internal instances now inherit the outer instance's defaults, andFilesexposes them through a new read-onlydefaultsgetter for plugins that need to do the same. - 5bc70b1: The fs adapter no longer corrupts or fails concurrent uploads to the same key. Two uploads that landed in the same millisecond shared one staging file, so one of them failed with a spurious
NotFoundfrom its rename and the survivor's stored etag came from the other call's bytes. Each upload now stages under a per-call unique name, stages its sidecar the same way, and commits both under a per-key lock, so every concurrent upload resolves and the final body and its etag always belong to the same call. - 5bc70b1: The fs adapter's in-flight upload staging files no longer surface as objects. A crash between writing the staging file and renaming it into place, or a
list()racing an upload, used to expose a<key>.<pid>.<ms>.tmpentry whose download served half-written bytes. Staging files now use the reserved.fls-tmpsuffix, whichlist()skips and which keys can no longer target, alongside the existing.meta.jsonand.fls-partreservations. - 5bc70b1: The fs adapter's write paths now enforce the same symlink boundary as its read paths.
upload,delete,move,copy(destination), and resumable uploads previously followed a symlinked directory inside the root to wherever it pointed, so a link to a directory outside the root let a key write or unlink files there even thoughdownload/head/existsrejected the same key. Those operations now resolve the nearest existing ancestor through symlinks before touching anything, and reject keys that resolve outside the adapter root with the existing "resolves outside adapter root"Providererror. - 5bc70b1: The FTP adapter now restores the working directory even when creating a nested directory fails partway through. basic-ftp's
ensureDirchanges into the tree one segment at a time, and the adapter only restored the original directory after a successful walk, so a refusedmkdirleft a reused connection parked inside the tree and later relative paths resolved against the wrong directory. - 5bc70b1: FTP resumable uploads (
multipart: trueor anUploadControl) now create the destination's parent directory before the first chunk, matching plainupload()andmove(). Previouslyupload("videos/clip.mp4", body, { multipart: true })with novideos/directory failed on the firstAPPEwith a 550 that surfaced asNotFound. - 5bc70b1: The gateway's presign upload tokens are now bound to the endpoint query they were minted under. With a per-request
filesfactory that picks the instance from the query string (the?bucket=pattern), a token minted under one bucket could previously be replayed against the proxy upload endpoint with the bucket switched, and the bytes landed in an instance whereauthorizewould have refused the upload; thePUT ?op=proxypath never re-ranauthorizeand the token carried no memory of the query. The token now records the request's non-routing query (everything exceptop,key, andtoken, in a canonical sorted form), and both the proxy upload andcompleterefuse a token presented under a different query with a 401Unauthorizederror. Single-bucket gateways with a bare endpoint are unaffected: an empty bound query matches an empty query. - 5bc70b1: The gateway's
searchop now matches against the caller-facing key whenauthorizereturns akeyPrefixscope, the same waylistalready returns unscoped keys. Previously the pattern was tested against the full storage key with the scope prefix still attached, so a client scoped tousers/1/searching*.png,a.pngwithmatch: "exact", or^aas a regex got no matches forusers/1/a.png. The handler also validatesmatchagainstglob,regex,substring, andexactand answers a 422 for anything else instead of silently treating the value as a regex. - 5bc70b1: Overwriting a key on Google Drive now replaces its metadata instead of merging it with the previous version's. Drive merges
appPropertieson update and only removes a key when it is sent asnull, so re-uploading withmetadata: { b: "2" }after{ a: "1" }read back as{ a: "1", b: "2" }(and a droppedcacheControllingered) while every other adapter yields{ b: "2" }. Both buffered and resumable overwrites now clear the stale keys explicitly. - 5bc70b1: The memory adapter no longer hands out its own stored bytes on read.
download(),head(), and ranged downloads passed the store'sUint8Array(or a view over it) straight into the returned file, andstream()enqueues that exact array, so a reader mutating a chunk silently corrupted the stored object without changing its etag. Read paths now copy the bytes out, matching the value semantics uploads already had. - 5bc70b1: Fix
onedrive()andsharepoint()copy()surfacing every failure as a genericProvidererror (copy returned 404 without monitor URL). The Graph client returns raw responses without checking their status, so the adapter now classifies non-2xx copy responses itself: a 404 becomesNotFound, 409/412 becomeConflict, and 401/403 becomeUnauthorized, with the Graph error message preserved, so retry and failover treat them correctly. - 5bc70b1: Fix
onedrive()andsharepoint()rejecting every chunk whenmultipart.partSizeexceeded Graph's 60 MiB upload-session fragment limit (for examplepartSize: 100 MiB). The requested part size is now clamped to the largest 320 KiB multiple at or below 60 MiB. - 5bc70b1: Fix
onedrive()(and thereforesharepoint()) streaming downloads throwingERR_INVALID_ARG_TYPEin every real runtime. The Graph client resolvesResponseType.STREAMwith the fetch Response body, which is already a webReadableStream, and the adapter was unconditionally passing it throughReadable.toWeb(). It now only converts when the client actually hands back a NodeReadable. - 5bc70b1: Fixed
createResponsesFileToolsemitting a schema that OpenAI rejects with a 400 when a tool override setsstrict: true. Strict tools now list every property inrequired, mark optional fields nullable, setadditionalProperties: falseon every object, and drop free-form maps (souploadFilehas nometadatafield under strict mode);executetreatsnullarguments for optional fields as absent. - 5bc70b1: Fixed the PocketBase adapter's
url()percent-encoding the slashes of nested keys (docs/a.txtbecamedocs%2Fa.txt) whenpublicBaseUrlis configured. Keys are now encoded per path segment, matching every other adapter. - 5bc70b1: The R2 HTTP adapter now forwards per-operation options (
signal, and thereforetimeout) forcopy,delete,exists, andhead. The lazy-loaded proxy over the inner S3 adapter dropped the third argument for those four verbs, so an abort signal never reached the underlying request even thoughdownload,list, anduploadon the same adapter honored it. - 5bc70b1:
useList,useFile, anduseSearchfromfiles-sdk/reactnow refetch whenendpointchanges. The hooks rebuilt their client on a new endpoint but only re-ran the query when the call options changed, so switching from/api/filesto/api/files?bucket=imageskept showing the previous endpoint's data until a manualrefetch(). The client is now part of the query's dependencies and is rebound only byendpoint;fetchImplis read live on each request likeheaders, so passing it inline does not refetch on every render. - 5bc70b1: S3 multipart, progress-reporting, and unsized-stream uploads now honor a signal that was already aborted by the time the upload started. The lib-storage path attached only an
"abort"listener, which never fires for a signal that flipped during the body normalization and lazy import that run first, so the upload proceeded and the object landed after the caller had been told it was aborted. The upload now aborts immediately and rejects with the usualabortederror. - 5bc70b1: The SFTP adapter now uploads into dot-prefixed directories such as
.well-known/acme-challenge/tokencorrectly. ssh2-sftp-client treats any relative path starting with.as if it began with./and strips two characters, so with the default root the adapter createdell-known/acme-challengeand the following write failed with a bogusNotFound. Relative parent directories are now passed with an explicit./anchor; absolute and already-anchored paths are unchanged. - 5bc70b1: Fix
sharepoint()not supporting resumable uploads even though the underlying OneDrive adapter does:upload(key, body, { control })threw an unsupported-operation error andfiles.capabilities.multipartreportedfalse. The wrapper now forwardsresumableUploadto the inner Graph upload-session driver, created lazily after site and drive resolution like every other verb. - 5bc70b1: Fixed
softDelete()'s whole-trashpurge()silently swallowing per-key delete failures. It emptied the trash through the bulkdelete, which collects errors instead of throwing, and discarded the result, sopurge()resolved whiletrashed()still listed the key. It now removes everything it can and then throws aFilesErrornaming how many objects failed, with the first failure as itscause, matching howpurge(key)already surfaced errors. - 5bc70b1: Fixed the Supabase adapter's
list()reporting Supabase's system metadata block (eTag, size, mimetype, cacheControl, contentLength, lastModified) as usermetadataon every item, whichhead()anddownload()never reported. List items now only surface user metadata when the listing response carries it underuser_metadata. - 5bc70b1: Fixed the UploadThing adapter resolving the lazy bodies of
list()items with an error page as file contents when the file had been deleted or the request was rejected. Those reads now check the response status likedownload()andhead()do, throwingNotFoundon 404 andProviderotherwise. - 5bc70b1: Fixed the Vercel Blob adapter resolving the lazy bodies of
head()andlist()results with the CDN's error page as file contents when the blob had been deleted or the request was rejected. Those reads now check the response status likedownload()does, throwingNotFoundon 404 andProviderotherwise. - 5bc70b1: Fixed
versioning()destroying the version being restored when alimitis set.restore()snapshots the current bytes first, and that snapshot could push the oldest kept version past the limit; the plugin pruned it before copying it back, so the restore failed withNotFoundand the version was gone for good (withlimit: 1no restore ever worked). Copies and moves now enforce the limit only after the operation has landed, so restoring the oldest kept version succeeds and the pre-restore bytes take the freed slot. - 5bc70b1: Fix
webdav()streaming downloads failing withstream.getReader is not a functionunder Node. Thewebdavpackage routes requests through node-fetch there, whose Response body is a NodeReadablerather than a web stream; the adapter now normalizes it to a webReadableStreamsodownload(key, { as: "stream" })works in Node as well as Bun and the browser. - 5bc70b1: The
zip()plugin now requests each entry's body as a stream when building an archive, so large files are no longer fully buffered in memory before being written. Archive contents are unchanged.