files-sdk@3.0.0
Major Changes
-
0223b30:
files-sdk/cacheno longer shares entries between instances. Store keys now include the instanceprefix, and a customstorerequires anamespacenaming the bucket it caches (cache({ store, namespace: "uploads-prod" })); without one,cache()throwsInvalid. Reusing onecache()on an instance with a different adapter orprefixalso throwsInvalid, 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/apigateway now refuses client keys andlist/searchprefixes inside theversioning()store (.versions/) or thesoftDelete()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 authorizekeyPrefix, a tenant’s own folders with those names stay ordinary keys. -
0223b30: The Google Drive adapter (
files-sdk/google-drive) now refusesupload({ cacheControl })withUnsupported(capabilities.cacheControlisfalse): Drive never serves it, and the value was silently dropped. It also declaressignedUpload.contentType: true, since the session binds the type, and no longer offers resumable uploads when built from a pre-builtclient, where they always threw. -
0223b30:
signedUploadUrl({ minSize })withoutmaxSizenow throwsUnsupportedonfiles-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. Ons3(), passmaxSizetoo to get a POST policy that enforces both.minSize: 0still returns a PUT. -
40a3694: The array (bulk) forms of
upload,download,head, anddeletenow resolve to one shape,BulkResult<T>={ results: T[]; errors?: BulkError[] }, instead of four differently named arrays.- Field renames: read
resultswhere you readuploaded,downloaded,files, ordeleted.exists([…])keeps its{ existing, missing, errors? }split. - Type changes:
UploadManyResult,DownloadManyResult,HeadManyResult, andDeleteManyResultare now aliases ofBulkResult.DeleteManyOptionsis an alias ofBulkOptions, andDeleteManyErroris removed in favour ofBulkError(the same{ key, error }shape). stopOnErroron a bulkdelete: 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.deleteManyreturns{ results, errors? }. - Gateway, client, CLI, and MCP: the
files-sdk/apiwire,files-sdk/client, theuseFilesbindings, and the CLI and MCP bulk output use the same shape.
- Field renames: read
-
58e15bc: Adapters now declare what they can do as one
capabilitiesobject, andfiles.capabilitiesgains the fields the old flags couldn’t express.- Custom adapters: the separate
supportsRange,supportsDelimiter,supportsMetadata,supportsCacheControl,supportsServerSideCopy,reportsUploadProgress, andsignedUrlfields onAdapterare replaced bycapabilities: { rangeRead, delimiter, metadata, cacheControl, serverSideCopy, uploadProgress, signedUrl, signedUpload }. Every field is optional and defaults to unsupported.resumableandconditionalare still derived fromresumableUploadandconditional. Declare it per instance, so a constructor option that changes what the adapter can do (apublicBaseUrl, an access mode, thefetchclient) is reflected. Rename the old flags:supportsRange→rangeRead,supportsDelimiter: true→delimiter: "any"(or"slash"),supportsMetadata→metadata,supportsCacheControl→cacheControl,supportsServerSideCopy→serverSideCopy,reportsUploadProgress→uploadProgress, top-levelsignedUrl→capabilities.signedUrl. The old fields are ignored and anything you leave out is off: withoutsignedUrl.supported,url(key, { expiresIn })throwsUnsupportedbefore yoururl()runs; withoutsignedUpload, the gateway proxies every upload; withoutsignedUrl.dispositionorpublicUrl, it proxies downloads. files.capabilities:multipartis renamedresumable(it always describedupload({ control })).delimiteris now"any" | "slash" | false, since Vercel Blob, Netlify Blobs, Supabase, Dropbox, Box, OneDrive, and SharePoint only accept"/".signedUrlgainsexpiry: "exact" | "provider" | "none": Box, PocketBase, and Dropbox’s 4-hour temporary links are"provider", because the provider sets the lifetime. There’s a newsignedUpload: { supported, maxSize, contentType, maxExpiresIn? }describingsignedUploadUrl(), and plugins that refuse direct uploads (encryption,compression,dedup,validation,contentType) turn it off.- Gateway (
files-sdk/api): presigning uploads now followssignedUploadinstead of the downloadsignedUrlflag. An upload is presigned only when the adapter can bind the gateway’smaxUploadSizeand the file’s content type; otherwise it goes through the proxy, which enforces both. UploadThing in its default public-read mode now reportssignedUrl.supported: false(itsurl()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, ordelimiteris 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 examplerange downloads are not supported by the "encryption" plugin.
- Custom adapters: the separate
-
7ca626f:
FilesErrorgains two codes,InvalidandUnsupported, split out ofProvider, 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, anexpiresInpast a hard cap, missing or bad constructor config, and a file avalidation()rule rejects (ValidationErrorkeeps itsreason).Unsupported: the call is well-formed but this adapter, in this mode and with these plugins, can’t do it. That covers arange,metadata,cacheControl,delimiter, orcontrolthe adapter has no primitive for,url()without apublicBaseUrlon FTP/SFTP/WebDAV, asignedUploadUrl()maxSizeorcontentTypethe provider can’t bind, and the fail-closed refusals ofencryption(),compression(),dedup(),validation(), andcontentType().permanent: both new codes are alwayspermanentand are never retried or failed over.Provideris still the only retried code.versioning()’srestoreVersion()andsoftDelete()’srestoreTrashed()now throwNotFoundwhen there’s nothing to restore.- Code that branches on
error.code === "Provider"to catch bad input or unsupported options must also checkInvalid/Unsupported. - Gateway (
files-sdk/api): anInvaliderror is answered with a422and the wire codeValidation(it was a500), and anUnsupportedone with a422and the new wire codeUnsupported.files-sdk/clientand theuseFilesbindings turn both back into the matchingFilesErrorcode, and map any wire code they don’t know toProvider. When presigning an upload, the gateway falls back to its proxy only for anUnsupportedorInvalidrefusal; a backend failure or an abort now surfaces instead of being masked as a proxy target. - CLI:
filesnow exits5for aProviderfailure, so a script can tell “the backend failed, retry” from “the command is wrong”.Invalid,Unsupported, andReadOnlyexit2, like a usage error. A missing optional@modelcontextprotocol/sdkforfiles mcpis reported asUnsupported.
-
9765e54:
head(),list(),listAll(), andsearch()now return aFileInfo({ key, size, contentType, etag?, lastModified?, metadata? }), metadata with no body, instead of aStoredFile.- 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 explicitdownload(), which still returns aStoredFile. AStoredFileis now aFileInfoplus theFile-likename/typealiases and the body accessors. - Renamed fields: on head and list results, read
contentTypewhere you readtype, andkeywhere you readname. - Upload results:
UploadResultis now the sameFileInfoshape. - Plugin results:
versioning()’srestoreVersion()andsoftDelete()’srestoreTrashed()resolve to aFileInfo. - Custom adapters:
Adapter.head()andAdapter.list()returnFileInfo.createStoredFile()takes aFileInfo(withcontentType, nottype), and theStoredFileMetatype is removed. - Gateway, client, and UI: the
files-sdk/apiwire,files-sdk/client, theuseFilesbindings, and the shadcn registry components use the same shape:contentTypeinstead oftype. The client’supload()result is now aFileInfotoo, and the registry components’onSelect/file/renderPreviewvalues areFileInfo. - AI tools: the AI tool results (
getFileMetadata,listFiles,downloadFile) also carrycontentType. - CLI and MCP:
head,list,search, anddownloadoutput printscontentTypeinstead oftype, and noname.
- No more hidden downloads: in v2, head and list results carried
-
6682759: Remove the
defaultUrlExpiresInoption from the filesystem adapter (files-sdk/fs). It was accepted for backward compatibility and ignored, becauseurl()returns a permanentfile://orurlBaseUrllink that can’t expire. Delete it from yourfs({ … })options. The CLI’s global--default-url-expires-inflag no longer reaches the fs adapter, which never used it. -
6682759: Require Node.js 22 or later. Node 20 reached end of life in April 2026, and the package’s
enginesfield now reads>=22. Bun and the edge runtimes the gateways target are unaffected. No peer dependency range is narrowed: Nitro 2 / h3 1 stays supported because Nuxt 4 still runs on it. -
21dd625:
url(key, { expiresIn })now always means “give me a link that expires”, instead of sometimes returning a permanent link that silently ignored the request.- Adapters that can sign: S3 and the S3-compatible adapters, R2, GCS, Firebase, Azure, Supabase, and Bun’s S3 now return a signed URL for an explicit
expiresIneven when apublicBaseUrlis configured, the same wayresponseContentDispositionalready did. A plainurl(key)still returns the permanent CDN link. - Adapters that only have permanent links: Vercel Blob in public mode, UploadThing
public-read, Convex, Appwrite, the filesystem, and OneDrive / Google Drive in public-link mode now throw anUnsupportedFilesErrorfor an explicitexpiresIn, before the adapter is called. Before, they returned the permanent link. LeaveexpiresInout to get that link. - Provider-set lifetimes: Box, PocketBase, and Dropbox’s tokenized links keep a lifetime set by the provider (
signedUrl.expiry: "provider"). Box and Dropbox in their public-link modes (publicBaseUrl/publicByDefault) now return that tokenized link for an explicitexpiresIn, instead of the permanent share link. - Two new capabilities:
files.capabilities.publicUrlreports whether a plainurl(key)returns a permanent link on this instance.files.capabilities.signedUrl.dispositionreports whether aresponseContentDispositionis bound into the signed URL.- Custom adapters declare both in their
capabilities(they default tofalse).
- Gateway downloads (
files-sdk/api): a download now redirects to a signed URL only when the adapter can also bind the forcedContent-Disposition, and otherwise streams through the proxy. With no forced disposition and noauthorizelifetime cap, an adapter with a permanent public link is redirected to it. - Gateway
urloperation: it signs whenever the adapter can. On an adapter that can’t sign, a client-requestedexpiresInor anauthorizemaxExpiresInis answered with a422instead of a permanent link. signedUrlPolicy(): it no longer pins a missingexpiresInon an instance that can’t sign, which would now throw.
- Adapters that can sign: S3 and the S3-compatible adapters, R2, GCS, Firebase, Azure, Supabase, and Bun’s S3 now return a signed URL for an explicit
Minor Changes
-
d70c0d2: The Dropbox adapter (
files-sdk/dropbox) now forwards the abort signal to every Dropbox API request. Cancelling a call, or hitting itstimeout, aborts the in-flight upload, download, listing, copy, delete, metadata, link, or resumable-chunk request instead of letting it run to completion in the background. This uses the SDK’s per-request transport options (dropbox10.42 and later); older versions ignore them and behave as before.Buffered uploads over Dropbox’s 150 MB single-call limit now use a concurrent upload session when
dropbox10.47 or later is installed:multipart.concurrencychunks (default 4, matching the S3 adapter) upload in parallel through the SDK’s newuploadFilehelper, with retries left to theFileswrapper’sretriesandonRetry. Passmultipart: { concurrency: 1 }to keep the sequential session. Stream bodies and resumable uploads stay sequential, and olderdropboxversions keep the sequential session for every upload. The peer range is unchanged. -
44b0a35:
files-sdk/eventsnow reads notifications from Backblaze B2, Tigris, Supabase (a Database Webhook onstorage.objects), Cloudinary, Appwrite and Box, from S3 delivered by SNS over HTTPS, and from Storj, which publishes S3-format events to Google Pub/Sub. Thes3format unwraps a Pub/Sub message, whether pushed, pulled, or handed over by the Node client library. The matching adapters pick their format automatically.webhook()verifies each provider’s own signature withverify: { secret }:- B2: HMAC-SHA256.
- Cloudinary: SHA-1 or SHA-256, checked for freshness.
- Appwrite: HMAC-SHA1 over the configured URL, so pass
url. - Box: primary or secondary key, checked for freshness.
verify: { sns }checks SNS message signatures against the certificate atSigningCertURL. That URL must be an HTTPS SNS host on the default port, with no credentials, query or fragment.topicArn(one ARN or several) is required. SNS signs messages for any topic, including one an attacker owns, so the topic is checked on every message, subscription confirmations included. The signedTimestampmust be no older thanmaxAge(default one hour). Both checks run before any certificate is fetched, and only a few signing keys are cached. Withconfirm: trueit also confirms new subscriptions to the pinned topic. B2 hide markers, which a delete throughfiles-sdk/backblaze-b2produces, count as deletes. Box keys are rebuilt relative to the adapter’srootFolderId. Box reports a trashed file at its Trash location, soFILE.TRASHEDgets a key only for a file that sat directly in the root folder. Cloudinary events for another resource type are dropped. -
44b0a35: Add
files-sdk/events, a plugin that turns each provider’s bucket notifications into one event shape. Installevents()withcreateFilesandfiles.eventsgains:on(type, glob?, handler)forcreated/deletedevents, matched against the caller-facing key.dispatch(delivery)andparse(delivery)for queue consumers. They accept an SQS or Lambda event, an R2 Queue message, a Pub/Sub message, an EventBridge event, or an array of them.webhook({ verify }), an endpoint with the gateway’s{ handle }shape for providers that push over HTTP. It answers the Event Grid and CloudEvents handshakes and authenticates every delivery: with a shared token, or with a Google-signed OIDC token for Pub/Sub push. A Google token must match both theaudienceand the push subscription’s service account (email), and both are required, since anyone can mint a Google-signed token for any audience. A failed certificate or key fetch answers502and a throwing handler500, so the provider redelivers. A handler’s error message never reaches the response; it goes toonError. Other unexpected failures go to the webhook’s ownonError.
It reads S3 (SQS, Lambda, SNS → SQS, SNS → Lambda, EventBridge), MinIO and RustFS webhooks, Wasabi (through SNS), R2 Queues, GCS Pub/Sub and Eventarc, and Azure Event Grid in either schema. Adapters declare the format their provider sends as a new
capabilities.events, whichfiles.capabilities.eventsreports:s3()ands3Fetch()declare"s3"only against AWS, since an S3-compatible endpoint may not send S3’s shape, whileminio,rustfs,wasabi,r2,gcs,firebase-storageandazuredeclare theirs. Parsing on an adapter with no format throwsUnsupportedinstead of guessing, and a malformed delivery throwsInvalid.Delivery is at least once and out of order on every provider, so each event carries an
idto dedupe on, and an optionaldedupestore drops repeats. The id is the provider’s event id, or is built from the key and the provider’s sequencer, version, timestamp or ETag. A record with none of those gets a hash of itself, never the clock, so a redelivery always keeps its id. Events outside the instanceprefixare dropped. So are events for another bucket: thebucketfilter defaults to the adapter’s own bucket when it exposes one, andbucket: falseturns it off. Gateway uploads reach the same handlers, andevents({ sdk: true })adds writes made through the instance. Withsdk: true,events()must come before any plugin that maps storage keys or sizes; otherwise the instance throwsInvalidwhen it’s built. On an instance that reads a format,on()throws when a plugin refuses provider events, aswebhook()does. The memory adapter emits events natively, withsettled()for tests.Plugins gain an optional
eventhook for mapping provider events.dedup(),encryption(),compression(),versioning(),softDelete()andtiering()use it, so their internal keys and stored sizes don’t leak into events. ThefilesCLI gainsfiles events parse [file] --format <format>for debugging a payload. -
10674b2: Add
files.abortUpload(key, token)to discard a persisted resumable upload from its session token.UploadControl.abort()discards the provider-side session only while the control is driving an upload: a control rebuilt withUploadControl.from(token)in a new process has no adapter to call, so itsabort()marked it aborted and left the session behind (an S3 multipart upload kept its billed parts).abortUpload()takes the same logical key and thecontrol.toJSON()token and does whatabort()does mid-upload: S3 and the S3-compatible adapters issueAbortMultipartUpload, GCS, Firebase Storage, Google Drive, OneDrive, SharePoint, and Supabase cancel the session,fs, FTP, and SFTP remove the staged partial, and Appwrite deletes the partial file. Adapters whose provider has no cancel primitive (Azure, Dropbox, Cloudinary, Vercel Blob) resolve and let the session expire. A token for another key, bucket, or provider throwsInvalidbefore anything is discarded, a session that’s already gone resolves while a cancel the provider refuses rejects, and the call throwsReadOnlyon a read-only instance andUnsupportedon an adapter without resumable uploads.The Appwrite adapter (
files-sdk/appwrite) also now checks that a file is still partially uploaded before a discard deletes it, so aborting from a stale token, or after a resumed upload failed against a file that had since completed, no longer deletes the finished file. The resumable-upload docs now say plainly that the browser client (files-sdk/client,useFiles) can’t pause or resume across a reload in 3.x. -
44b0a35: Add an upload lifecycle hook to the gateway (
files-sdk/api).createFilesRouter({ onUploadComplete })runs on the server once per verified upload: incompletefor keyless uploads, after the gatewayheads the landed object and checksmaxUploadSize, and after a keyedupload(key, body)stores its body. Record the upload there instead of building a second endpoint and trusting the client to call it. The hook receives the caller-facingfilemetadata, itsstorageKey, the request, a stableuploadIdand how the bytes arrived (via).authorizecan now returncontext(the signed-in user, say), which reaches the hook typed.Whatever the hook returns comes back to the browser as the upload result’s
data, increateFilesClient, the React, Vue and SvelteuseFilesbindings (results anduploadsentries), and the registryDropzone/MultipartUploaderonUploadedcallbacks. Type it withuseFiles<InferUploadData<typeof router>>().Throwing from the hook rejects the upload: the gateway deletes the object and the client’s
upload()rejects with the error (UploadRejectedErrorgives a 422 with your message). A removal that fails is reported alongside the rejection. SetonRejected: "keep"to keep rejected objects, including ones the complete-timemaxUploadSizecheck refuses. Upload tokens are stateless, so a replayedcompletefires the hook again with the sameuploadId; pass acompletionsstore to make completions single-use. -
d70c0d2: Add a
regionoption to the Netlify Blobs adapter (files-sdk/netlify-blobs). Site-wide stores don’t read a region from the environment, so until now they always used the Netlify API’s default region rather than the site’s Functions region. Passregion: "eu-central-1"(or any region@netlify/blobssupports) to keep a store’s data next to the functions that read it. Deploy-scoped stores keep defaulting to the deploy’s region, and changingregionlater doesn’t move data a store already holds. No peer range change: every supported@netlify/blobsversion forwards the option. -
90fc706: Support Nitro 3 / h3 2 in the Nitro gateway binding (
files-sdk/nitro), alongside Nitro 2 / h3 1. h3 2 hands every runtime a WebRequestonevent.req, and the binding now passes it to the gateway as-is. Before, it readevent.node, which h3 2 only provides on Node, so the route failed with a 500 on Bun, Deno, and edge presets. On h3 1 nothing changes: the Node request is still marshalled, and in-process requests (localFetch, SSR$fetch) still read their body from the event. The binding no longer imports anything fromh3, including types, so it typechecks in Nitro 3 apps that only reach h3 throughnitro/h3.createRouteHandler‘s parameter is the new structuralNitroEventtype, which both majors’H3Eventsatisfy, and theh3peer range widens to^1.0.0 || ^2.0.0. -
6682759: An empty
prefix("") or an all-slash one ("/") passed tonew Files({ prefix })now means no prefix, the same as leaving the option out, instead of throwing “prefix must be a non-empty string”. This keepsprefix: process.env.FILES_PREFIX ?? ""working when the variable is unset.
Patch Changes
-
5cdab4f: Fix
createFileTools()fromfiles-sdk/ai-sdknot typechecking with AI SDK 7.FileToolswas declared as aninterface, which has no implicit index signature, so it wasn’t assignable toai’sToolSetandgenerateText({ tools: createFileTools({ files }) }),streamText, andnew ToolLoopAgent({ tools })failed to compile with “Index signature for type ‘string’ is missing”.FileToolsis now a type alias with the same members.AgentsFileToolsfromfiles-sdk/openaigets the same change, so it’s assignable to aRecord<string, Tool>. The Vercel AI SDK docs also now spell out that an AI SDK 7toolApprovalfunction overrides every tool’sneedsApproval, and that returningundefinedfrom it runs a write without approval. -
647b04a: The published build no longer imports
node:modulefrom the rootfiles-sdkentry,files-sdk/r2, or the other adapter and plugin entries. Bun 1.4.0’s bundler emitted an unusedcreateRequireshim chunk and imported it from almost every entry, so a Cloudflare Worker without thenodejs_compatflag failed to bundle Files SDK withCould not resolve "node:module". The package is now built with Bun 1.4.2, which doesn’t emit the shim, and a build-output test bundles the Worker-facing subpaths the way Wrangler does to keep it that way. -
d70c0d2: Fix error classification in the Bunny Storage adapter (
files-sdk/bunny-storage) on@bunny.net/storage-sdk0.3.2, which started putting the key into its 400 error message. A rejected request for a key containing words like “not found”, “forbidden”, or “conflict” was misread asNotFound,Unauthorized, orConflict; the SDK’s own message templates are now matched exactly first, so a 400 stays aProvidererror whatever the key says. -
de04cc9: Security: the
files-sdk/apigateway’scompletestep now checks that an upload token was issued for the caller. It verified the token’s signature but never compared its key against the calling request’s authorizedkeyPrefix, so one tenant could complete another tenant’s token and read back that upload’s full storage key and metadata. A token whose key lies outside the caller’s prefix now gets anUnauthorizedentry (“upload token was not issued for this caller”) that echoes only the key the caller sent. The proxy uploadPUTis unchanged: like a presigned URL, its token alone authorizes writing the one keypresignminted until it expires, and the authorization docs now say so. -
de04cc9: The
files-sdk/apigateway no longer breaks on adapters that can’t setresponseContentDispositionon their URLs (Vercel Blob, Box, Dropbox, PocketBase, UploadThing, Cloudinary, the localfsadapter, and the others whoseurl()refuses the option). Before, the defaultdownloadreturned a 500 on private Vercel Blob and the other signing adapters among them, and theurloperation returned a 500 on all of them. NowdownloadMode: "auto"proxies such a download soContent-Disposition: attachmentstill applies ("redirect"still surfaces the refusal), and whenauthorizereturns{ disposition: "inline" }the gateway mints the URL without a disposition for bothurland the download redirect. Without that inline policy, theurloperation still fails (a422Unsupported), because the gateway can’t guaranteeattachment, but the error now explains how to proceed: allow inline fromauthorize, or usedownload. The adapters’ refusal keeps its message and is now anUnsupportederror, so it is never retried or failed over. -
de04cc9: Proxied downloads from the
files-sdk/apigateway play media and send proper validators. With the defaultonUnsupportedRange: "reject", aRange: bytes=0-request to an adapter that can’t serve ranges got a416, which broke every<video>and<audio>element (browsers open media with that range). The whole body satisfies it, so it now gets the full object with a200; any other range on such an adapter is still a416. TheETagheader is now always a quoted entity-tag, even when the adapter reports it bare as the S3 family does, andIf-Rangematches either form. Proxied responses also sendLast-Modifiedwhen the adapter knows it.files-sdk/clientkeeps reporting the adapter’s own etag on proxied downloads, ashead()does. -
de04cc9: The
files-sdk/apigateway enforcesmaxUploadSizeat both ends of a keyless upload.presignnow refuses a file whose declaredsizeis over the limit with a422(reason: "size") before signing anything. The declared size is only advisory, socompletestill checks the stored object, and when that object is over the token’s limit it is now deleted instead of left in storage behind the error entry. The key was minted by the server for that upload alone, so nothing else is touched; a failed removal is reported in the entry’s message. -
d70c0d2: Accept
@googleapis/drive25 and 26 as peers of the Google Drive adapter (files-sdk/google-drive). Both majors only raise the package’s own Node.js floor to 22 (23 and 24 were never published), matching Files SDK 3’s Node 22 floor, and the Drive v3 surface the adapter uses is unchanged. -
d974716: The package’s
homepagenow points to the documentation site, https://files-sdk.dev, instead of the GitHub README, so the npm page and other registries link straight to the docs. -
134f880: Give
head()of a missing key a useful error message on the S3 adapter (files-sdk/s3) and every adapter built on it. A HEAD 404 has no body, so the AWS SDK’s placeholder messageUnknownErrorcame through as theNotFounderror’s message. It now reads “The specified key does not exist.”, the same text adownload()of that key gets from S3. Thefetchengine (files-sdk/s3-fetch, and R2, MinIO, and RustFS onclient: "fetch") uses the same message instead of the generic “Not found”. Other bodyless failures on the AWS SDK path (a HEAD 403 or 500) fall back to the standardUnauthorized/ provider message instead ofUnknownError, andmapS3Errortreats the placeholder as no message. -
2cbf291: Bind
contentTypeinto presigned PUT URLs from the S3 adapter (files-sdk/s3).signedUploadUrl(key, { contentType })withoutmaxSizereturned a URL signed over thehostheader only, because the AWS presigner leavesContent-Typeout of the signature by default. TheContent-Typeheader it handed back was advisory, and a client could upload any type, despitecontentTypebeing documented as bound into the signature. The URL now signscontent-typetoo, so an upload with a different Content-Type is rejected with a 403. This covers every S3-compatible adapter built ons3()(Spaces, Wasabi, Backblaze B2, Tigris, Hetzner, and the rest) and R2, MinIO, and RustFS onclient: "aws-sdk"; thefetchengine already signed it. Send the returnedheadersunchanged with the upload. -
90fc706: Accept
@sveltejs/kit3 as a peer of the SvelteKit gateway binding (files-sdk/sveltekit), alongside 2.createRouteHandleronly relies on theRequestHandlertype, which is unchanged in SvelteKit 3, so the same{ GET, POST, PUT }exports work on either major. -
dc0b34e: CLI, MCP, and AI tool fixes:
- Bad integer flags and local file problems exit
2instead of the retryable5, and a missing provider SDK isUnsupportedwith thenpm installcommand. files events parseuses the configured provider alongside--format, accepts--header(needed for Appwrite), and no longer waits on a terminal.- The MCP and AI tools describe
expiresInand capabilities the v3 way, reportmaxBytesand bad base64 asInvalid, and the MCPuploadinfers the content type from the key.
- Bad integer flags and local file problems exit
-
dc0b34e: Cloud adapter fixes:
- Supabase: user metadata now round-trips exactly on every read (keys were camelCased or dropped, so
encryption()returned ciphertext), there are no resumable uploads with a pre-builtclient, andabortUpload()checks the cancel status. - Cloudinary: signed URLs, private reads, and resumable uploads need an API key and secret, and are
Unsupportedwithout them instead of failing on every call. - GCS, Firebase Storage, and UploadThing reject an expiry over 7 days with
Invalid. - UploadThing, Vercel Blob, and Cloudinary map direct-read HTTP statuses to the standard codes.
- Netlify Blobs config errors and PocketBase records without a file are
Invalid, and a Bunny Storage delete with a read-only key isUnauthorized. - Appwrite offers resumable uploads only with an API key.
- Supabase: user metadata now round-trips exactly on every read (keys were camelCased or dropped, so
-
dc0b34e: Core fixes:
- A plugin that only declares
capabilitiesis now enforced like one that also wraps. delete(keys, { stopOnError: true })goes key by key with or without plugins, so both return the same result.abortUpload()firesonAction,onError, andonRetry(type: "abortUpload"), and rejects when the provider refuses the cancel (GCS, Firebase Storage, Google Drive, OneDrive, SharePoint, Supabase) instead of reporting success.sync({ prune: true, signal })stops pruning once the signal aborts.- A plugin’s
extendcan no longer replace a symbol-keyedFilesmember.
- A plugin that only declares
-
dc0b34e: File and drive adapter fixes:
fs:publicUrlis declared only withurlBaseUrl, so the gateway no longer redirects tofile://paths. Directories are no longer objects:head,exists,download,copy, andmovereportNotFound,deleteis a no-op, and writing over one is aConflict.- WebDAV, Dropbox, OneDrive, and SharePoint no longer delete or copy whole folders: a folder key is a no-op for
deleteandNotFoundas a copy source. Dropbox and OneDrive refuse empty,/, and trailing-slash keys. - Dropbox, Box, OneDrive, and SharePoint
copyandmovenow overwrite an existing file, like the other adapters. OneDrive copy failures are classified by Graph error code, and a copy timeout is no longer retried. - Box recovers when a key is deleted and re-created elsewhere.
- Out-of-order resumable calls on Dropbox and OneDrive are
Invalid. memoryrefusesurl({ expiresIn })when called directly.
-
dc0b34e: Gateway and browser client fixes (
files-sdk/api,files-sdk/client, and the framework bindings):- Errors that aren’t a
FilesError(fromauthorize,onUploadComplete, or acompletionsstore) now reach the client as a generic 500, and the original goes to a newonError(error, req)option. Storage keys in error messages are rewritten to the client’s key, so thekeyPrefixdoesn’t leak. - Proxied downloads send
Cache-Control: private, no-store, and content a browser could run (anything but images, media, and PDF) gets a sandboxingContent-Security-Policy. - With a
completionsstore, the proxy refuses anotherPUToncecompletehas accepted the upload (409). It also refuses tokens minted for direct-to-storage uploads. completeaccepts a token for a grace period after it expires (completeGracePeriod, default one hour), and proxy tokens are no longer capped by the adapter’s signed-upload limit.- Keys with backslash
..segments are refused. authorizenow receives the declared filename,size, andtypefor uploads, andfilterKeysalso coversversions,restore-version,restore-trashed, andcomplete.searchreads at mostmaxSearchScankeys (default 10,000) and reportstruncated; the client’ssearch()returns{ truncated }(SearchSummary).- Corrected codes: an oversized upload at
completeisValidation(reasonsize), a missing plugin isUnsupported, and a 416 carries an error body and maps toInvalidon the client. trashedand a scopedpurgeread only the caller’s part of the trash (softDelete().trashed({ prefix })),presignsigns at mostmaxConcurrencytargets at once, bulk operations don’t start for a disconnected client, andautodownloads fall back to the proxy when a redirect URL is refused.UploadRejectedErrorunderfiles-sdk/nestjsnow answers 422 instead of 500.files-sdk/versioningprunes history only after a write succeeds.
- Errors that aren’t a
-
dc0b34e: Plugin fixes:
files-sdk/tieringandfiles-sdk/failoveradvertise only what every backend supports, so an unsupported option fails up front instead of only on some keys or after a failover. Tiering also reportsserverSideCopy: false, andevents: falsewithfallback: true.files-sdk/signed-url-policynarrowssignedUpload,publicUrl, andsignedUrlto what still works under the policy.files-sdk/cacheinvalidates a key on storage events.files-sdk/compressionreports an unknown stored algorithm asUnsupported.files-sdk/dedupkeepssizeandetagon events for objects it didn’t write.
-
dc0b34e: S3 family fixes:
- Presigned PUT URLs on AWS no longer carry an empty-body checksum (
x-amz-checksum-crc32=AAAAAA==) that made S3 reject every real upload. - Metadata that can’t travel as a header (non-Latin-1 or control characters) and out-of-order resumable calls are
Invalidinstead of a retriedProvidererror. - Presigned POST uploads enforce the 7-day expiry limit.
- An explicit
amazonaws.comendpoint (orAWS_ENDPOINT_URL*) counts as AWS for events and conditional writes on both engines. - Backblaze B2 refuses
maxSizeup front, since it has no presigned POST. - Bun S3 reports a 401/403 on HEAD as
Unauthorizedinstead of retrying it.
- Presigned PUT URLs on AWS no longer carry an empty-body checksum (
-
51e7bd1: Report failed keys through
onProgressintransfer()andsync(), sodonereachestotal. Only successful and skipped keys fired a progress event, so a run with any failures never reached the documented denominator and a progress bar stalled short of 100%. A key that fails now fires an event withstatus: "failed"(its error is still in the result’serrors), andsync()does the same for a failed prune.TransferProgress["status"]gains"failed"alongside"transferred"and"skipped", andSyncProgress["status"]alongside"uploaded","skipped", and"deleted"; an exhaustiveswitchover the status needs the new case. UnderstopOnErrorthe run still stops at the first failure, so the keys after it never settle. -
86ec31c: Fix the Vercel Blob adapter (
files-sdk/vercel-blob) choosingBLOB_READ_WRITE_TOKENover OIDC on Vercel Functions. When a project had bothBLOB_STORE_IDandBLOB_READ_WRITE_TOKEN, the adapter saw no OIDC token inprocess.env(on Functions it arrives per request, in thex-vercel-oidc-tokenheader) and passed the long-lived read-write token explicitly, so@vercel/blobnever looked for the request’s token. With a store id and notokenoption, the adapter now always lets@vercel/blobpick the credential on each call, the way it does when called directly: the request header first, thenVERCEL_OIDC_TOKEN(refreshed when expired on@vercel/blob2.5 and later), thenBLOB_READ_WRITE_TOKEN. An explicittokenstill wins, and an explicitoidcTokenwith no store id still throws. When only a store id is configured and no token turns up for a call, that operation now throws anInvalidmissing credentialserror, never retried, instead of@vercel/blob’s “No blob credentials found”, which becomes itscause. -
4a2b2b6: Fix OIDC authentication in the Vercel Blob adapter (
files-sdk/vercel-blob) on Vercel Functions. There the OIDC token arrives per request, in thex-vercel-oidc-tokenheader, rather than inprocess.env. With an OIDC-only store (BLOB_STORE_IDand noBLOB_READ_WRITE_TOKEN),vercelBlob()threw “missing credentials” at construction even though@vercel/blobcould authenticate. The adapter now passes the store id and lets@vercel/blobfind the OIDC token itself, for environment tokens too. So a fresh request-header token wins over a staleVERCEL_OIDC_TOKEN, and on@vercel/blob2.5 and later an expired local token fromvercel env pullis refreshed. An explicitoidcTokenoption is still used as given. -
3a8da5e: List every current Wasabi region in the
regionoption’s documentation for the Wasabi adapter (files-sdk/wasabi). The list was missingus-west-2(San Jose),eu-west-3(United Kingdom), andeu-south-1(Milan). The adapter already built the right endpoint for them, since every Wasabi region useshttps://s3.<region>.wasabisys.com; only the editor hint was out of date. -
bde4f74: Fix Cloudflare Worker builds of the R2, MinIO, and RustFS adapters (
files-sdk/r2,files-sdk/minio,files-sdk/rustfs) without the@aws-sdk/*packages installed. A Worker that only used the R2 binding, hybrid signing, orclient: "fetch"failedwrangler deployandwrangler devwithCould not resolve "@aws-sdk/client-s3"(and the same for@aws-sdk/lib-storage,@aws-sdk/s3-presigned-post, and@aws-sdk/s3-request-presigner), because Wrangler’s bundler resolves the dynamic imports behind the lazily loaded"aws-sdk"engine even when they never run. Those imports are now written so bundlers treat a missing package as a run-time failure: Wrangler, esbuild, Bun, and rolldown build without them, andfiles-sdkalone is enough, as the docs already said. When the packages are installed they are still resolved and bundled, soclient: "aws-sdk"keeps working everywhere it did, including Workers with aDOMParserpolyfill. If the"aws-sdk"engine runs without@aws-sdk/client-s3and the two presigners, its first call now rejects with aFilesErrorthat names them and suggestsclient: "fetch", rather than a bare module-not-found error or, under Next.js’s webpack, which swaps a missing optional peer for an empty module, “is not a constructor”.