Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

files-sdk@3.0.0

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 a FileInfo.
    • Custom adapters: Adapter.head() and Adapter.list() return FileInfo. createStoredFile() takes a FileInfo (with contentType, not type), and the StoredFileMeta type is removed.
    • Gateway, client, and UI: the files-sdk/api wire, files-sdk/client, the useFiles bindings, and the shadcn registry components use the same shape: contentType instead of type. The client’s upload() result is now a FileInfo too, and the registry components’ onSelect / file / renderPreview values are FileInfo.
    • AI tools: the AI tool results (getFileMetadata, listFiles, downloadFile) also carry contentType.
    • CLI and MCP: head, list, search, and download output prints contentType instead of type, and no name.
  • 6682759: Remove the defaultUrlExpiresIn option from the filesystem adapter (files-sdk/fs). It was accepted for backward compatibility and ignored, because url() returns a permanent file:// or urlBaseUrl link that can’t expire. Delete it from your fs({ … }) options. The CLI’s global --default-url-expires-in flag 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 engines field 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 expiresIn even when a publicBaseUrl is configured, the same way responseContentDisposition already did. A plain url(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 an Unsupported FilesError for an explicit expiresIn, before the adapter is called. Before, they returned the permanent link. Leave expiresIn out 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 explicit expiresIn, instead of the permanent share link.
    • Two new capabilities:
      • files.capabilities.publicUrl reports whether a plain url(key) returns a permanent link on this instance.
      • files.capabilities.signedUrl.disposition reports whether a responseContentDisposition is bound into the signed URL.
      • Custom adapters declare both in their capabilities (they default to false).
    • Gateway downloads (files-sdk/api): a download now redirects to a signed URL only when the adapter can also bind the forced Content-Disposition, and otherwise streams through the proxy. With no forced disposition and no authorize lifetime cap, an adapter with a permanent public link is redirected to it.
    • Gateway url operation: it signs whenever the adapter can. On an adapter that can’t sign, a client-requested expiresIn or an authorize maxExpiresIn is answered with a 422 instead of a permanent link.
    • signedUrlPolicy(): it no longer pins a missing expiresIn on an instance that can’t sign, which would now throw.

Minor Changes

  • d70c0d2: The Dropbox adapter (files-sdk/dropbox) now forwards the abort signal to every Dropbox API request. Cancelling a call, or hitting its timeout, 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 (dropbox 10.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 dropbox 10.47 or later is installed: multipart.concurrency chunks (default 4, matching the S3 adapter) upload in parallel through the SDK’s new uploadFile helper, with retries left to the Files wrapper’s retries and onRetry. Pass multipart: { concurrency: 1 } to keep the sequential session. Stream bodies and resumable uploads stay sequential, and older dropbox versions keep the sequential session for every upload. The peer range is unchanged.

  • 44b0a35: files-sdk/events now reads notifications from Backblaze B2, Tigris, Supabase (a Database Webhook on storage.objects), Cloudinary, Appwrite and Box, from S3 delivered by SNS over HTTPS, and from Storj, which publishes S3-format events to Google Pub/Sub. The s3 format 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 with verify: { 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 at SigningCertURL. 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 signed Timestamp must be no older than maxAge (default one hour). Both checks run before any certificate is fetched, and only a few signing keys are cached. With confirm: true it also confirms new subscriptions to the pinned topic. B2 hide markers, which a delete through files-sdk/backblaze-b2 produces, count as deletes. Box keys are rebuilt relative to the adapter’s rootFolderId. Box reports a trashed file at its Trash location, so FILE.TRASHED gets 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. Install events() with createFiles and files.events gains:

    • on(type, glob?, handler) for created / deleted events, matched against the caller-facing key.
    • dispatch(delivery) and parse(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 the audience and 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 answers 502 and a throwing handler 500, so the provider redelivers. A handler’s error message never reaches the response; it goes to onError. Other unexpected failures go to the webhook’s own onError.

    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, which files.capabilities.events reports: s3() and s3Fetch() declare "s3" only against AWS, since an S3-compatible endpoint may not send S3’s shape, while minio, rustfs, wasabi, r2, gcs, firebase-storage and azure declare theirs. Parsing on an adapter with no format throws Unsupported instead of guessing, and a malformed delivery throws Invalid.

    Delivery is at least once and out of order on every provider, so each event carries an id to dedupe on, and an optional dedupe store 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 instance prefix are dropped. So are events for another bucket: the bucket filter defaults to the adapter’s own bucket when it exposes one, and bucket: false turns it off. Gateway uploads reach the same handlers, and events({ sdk: true }) adds writes made through the instance. With sdk: true, events() must come before any plugin that maps storage keys or sizes; otherwise the instance throws Invalid when it’s built. On an instance that reads a format, on() throws when a plugin refuses provider events, as webhook() does. The memory adapter emits events natively, with settled() for tests.

    Plugins gain an optional event hook for mapping provider events. dedup(), encryption(), compression(), versioning(), softDelete() and tiering() use it, so their internal keys and stored sizes don’t leak into events. The files CLI gains files 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 with UploadControl.from(token) in a new process has no adapter to call, so its abort() marked it aborted and left the session behind (an S3 multipart upload kept its billed parts). abortUpload() takes the same logical key and the control.toJSON() token and does what abort() does mid-upload: S3 and the S3-compatible adapters issue AbortMultipartUpload, 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 throws Invalid before anything is discarded, a session that’s already gone resolves while a cancel the provider refuses rejects, and the call throws ReadOnly on a read-only instance and Unsupported on 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: in complete for keyless uploads, after the gateway heads the landed object and checks maxUploadSize, and after a keyed upload(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-facing file metadata, its storageKey, the request, a stable uploadId and how the bytes arrived (via). authorize can now return context (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, in createFilesClient, the React, Vue and Svelte useFiles bindings (results and uploads entries), and the registry Dropzone / MultipartUploader onUploaded callbacks. Type it with useFiles<InferUploadData<typeof router>>().

    Throwing from the hook rejects the upload: the gateway deletes the object and the client’s upload() rejects with the error (UploadRejectedError gives a 422 with your message). A removal that fails is reported alongside the rejection. Set onRejected: "keep" to keep rejected objects, including ones the complete-time maxUploadSize check refuses. Upload tokens are stateless, so a replayed complete fires the hook again with the same uploadId; pass a completions store to make completions single-use.

  • d70c0d2: Add a region option 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. Pass region: "eu-central-1" (or any region @netlify/blobs supports) to keep a store’s data next to the functions that read it. Deploy-scoped stores keep defaulting to the deploy’s region, and changing region later doesn’t move data a store already holds. No peer range change: every supported @netlify/blobs version 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 Web Request on event.req, and the binding now passes it to the gateway as-is. Before, it read event.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 from h3, including types, so it typechecks in Nitro 3 apps that only reach h3 through nitro/h3. createRouteHandler‘s parameter is the new structural NitroEvent type, which both majors’ H3Event satisfy, and the h3 peer range widens to ^1.0.0 || ^2.0.0.

  • 6682759: An empty prefix ("") or an all-slash one ("/") passed to new Files({ prefix }) now means no prefix, the same as leaving the option out, instead of throwing “prefix must be a non-empty string”. This keeps prefix: process.env.FILES_PREFIX ?? "" working when the variable is unset.

Patch Changes

  • 5cdab4f: Fix createFileTools() from files-sdk/ai-sdk not typechecking with AI SDK 7. FileTools was declared as an interface, which has no implicit index signature, so it wasn’t assignable to ai’s ToolSet and generateText({ tools: createFileTools({ files }) }), streamText, and new ToolLoopAgent({ tools }) failed to compile with “Index signature for type ‘string’ is missing”. FileTools is now a type alias with the same members. AgentsFileTools from files-sdk/openai gets the same change, so it’s assignable to a Record<string, Tool>. The Vercel AI SDK docs also now spell out that an AI SDK 7 toolApproval function overrides every tool’s needsApproval, and that returning undefined from it runs a write without approval.

  • 647b04a: The published build no longer imports node:module from the root files-sdk entry, files-sdk/r2, or the other adapter and plugin entries. Bun 1.4.0’s bundler emitted an unused createRequire shim chunk and imported it from almost every entry, so a Cloudflare Worker without the nodejs_compat flag failed to bundle Files SDK with Could 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-sdk 0.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 as NotFound, Unauthorized, or Conflict; the SDK’s own message templates are now matched exactly first, so a 400 stays a Provider error whatever the key says.

  • de04cc9: Security: the files-sdk/api gateway’s complete step 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 authorized keyPrefix, 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 an Unauthorized entry (“upload token was not issued for this caller”) that echoes only the key the caller sent. The proxy upload PUT is unchanged: like a presigned URL, its token alone authorizes writing the one key presign minted until it expires, and the authorization docs now say so.

  • de04cc9: The files-sdk/api gateway no longer breaks on adapters that can’t set responseContentDisposition on their URLs (Vercel Blob, Box, Dropbox, PocketBase, UploadThing, Cloudinary, the local fs adapter, and the others whose url() refuses the option). Before, the default download returned a 500 on private Vercel Blob and the other signing adapters among them, and the url operation returned a 500 on all of them. Now downloadMode: "auto" proxies such a download so Content-Disposition: attachment still applies ("redirect" still surfaces the refusal), and when authorize returns { disposition: "inline" } the gateway mints the URL without a disposition for both url and the download redirect. Without that inline policy, the url operation still fails (a 422 Unsupported), because the gateway can’t guarantee attachment, but the error now explains how to proceed: allow inline from authorize, or use download. The adapters’ refusal keeps its message and is now an Unsupported error, so it is never retried or failed over.

  • de04cc9: Proxied downloads from the files-sdk/api gateway play media and send proper validators. With the default onUnsupportedRange: "reject", a Range: bytes=0- request to an adapter that can’t serve ranges got a 416, 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 a 200; any other range on such an adapter is still a 416. The ETag header is now always a quoted entity-tag, even when the adapter reports it bare as the S3 family does, and If-Range matches either form. Proxied responses also send Last-Modified when the adapter knows it. files-sdk/client keeps reporting the adapter’s own etag on proxied downloads, as head() does.

  • de04cc9: The files-sdk/api gateway enforces maxUploadSize at both ends of a keyless upload. presign now refuses a file whose declared size is over the limit with a 422 (reason: "size") before signing anything. The declared size is only advisory, so complete still 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/drive 25 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 homepage now 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 message UnknownError came through as the NotFound error’s message. It now reads “The specified key does not exist.”, the same text a download() of that key gets from S3. The fetch engine (files-sdk/s3-fetch, and R2, MinIO, and RustFS on client: "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 standard Unauthorized / provider message instead of UnknownError, and mapS3Error treats the placeholder as no message.

  • 2cbf291: Bind contentType into presigned PUT URLs from the S3 adapter (files-sdk/s3). signedUploadUrl(key, { contentType }) without maxSize returned a URL signed over the host header only, because the AWS presigner leaves Content-Type out of the signature by default. The Content-Type header it handed back was advisory, and a client could upload any type, despite contentType being documented as bound into the signature. The URL now signs content-type too, so an upload with a different Content-Type is rejected with a 403. This covers every S3-compatible adapter built on s3() (Spaces, Wasabi, Backblaze B2, Tigris, Hetzner, and the rest) and R2, MinIO, and RustFS on client: "aws-sdk"; the fetch engine already signed it. Send the returned headers unchanged with the upload.

  • 90fc706: Accept @sveltejs/kit 3 as a peer of the SvelteKit gateway binding (files-sdk/sveltekit), alongside 2. createRouteHandler only relies on the RequestHandler type, 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 2 instead of the retryable 5, and a missing provider SDK is Unsupported with the npm install command.
    • files events parse uses the configured provider alongside --format, accepts --header (needed for Appwrite), and no longer waits on a terminal.
    • The MCP and AI tools describe expiresIn and capabilities the v3 way, report maxBytes and bad base64 as Invalid, and the MCP upload infers the content type from the key.
  • 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-built client, and abortUpload() checks the cancel status.
    • Cloudinary: signed URLs, private reads, and resumable uploads need an API key and secret, and are Unsupported without 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 is Unauthorized.
    • Appwrite offers resumable uploads only with an API key.
  • dc0b34e: Core fixes:

    • A plugin that only declares capabilities is 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() fires onAction, onError, and onRetry (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 extend can no longer replace a symbol-keyed Files member.
  • dc0b34e: File and drive adapter fixes:

    • fs: publicUrl is declared only with urlBaseUrl, so the gateway no longer redirects to file:// paths. Directories are no longer objects: head, exists, download, copy, and move report NotFound, delete is a no-op, and writing over one is a Conflict.
    • WebDAV, Dropbox, OneDrive, and SharePoint no longer delete or copy whole folders: a folder key is a no-op for delete and NotFound as a copy source. Dropbox and OneDrive refuse empty, /, and trailing-slash keys.
    • Dropbox, Box, OneDrive, and SharePoint copy and move now 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.
    • memory refuses url({ 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 (from authorize, onUploadComplete, or a completions store) now reach the client as a generic 500, and the original goes to a new onError(error, req) option. Storage keys in error messages are rewritten to the client’s key, so the keyPrefix doesn’t leak.
    • Proxied downloads send Cache-Control: private, no-store, and content a browser could run (anything but images, media, and PDF) gets a sandboxing Content-Security-Policy.
    • With a completions store, the proxy refuses another PUT once complete has accepted the upload (409). It also refuses tokens minted for direct-to-storage uploads.
    • complete accepts 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.
    • authorize now receives the declared file name, size, and type for uploads, and filterKeys also covers versions, restore-version, restore-trashed, and complete.
    • search reads at most maxSearchScan keys (default 10,000) and reports truncated; the client’s search() returns { truncated } (SearchSummary).
    • Corrected codes: an oversized upload at complete is Validation (reason size), a missing plugin is Unsupported, and a 416 carries an error body and maps to Invalid on the client.
    • trashed and a scoped purge read only the caller’s part of the trash (softDelete().trashed({ prefix })), presign signs at most maxConcurrency targets at once, bulk operations don’t start for a disconnected client, and auto downloads fall back to the proxy when a redirect URL is refused.
    • UploadRejectedError under files-sdk/nestjs now answers 422 instead of 500.
    • files-sdk/versioning prunes history only after a write succeeds.
  • dc0b34e: Plugin fixes:

    • files-sdk/tiering and files-sdk/failover advertise only what every backend supports, so an unsupported option fails up front instead of only on some keys or after a failover. Tiering also reports serverSideCopy: false, and events: false with fallback: true.
    • files-sdk/signed-url-policy narrows signedUpload, publicUrl, and signedUrl to what still works under the policy.
    • files-sdk/cache invalidates a key on storage events.
    • files-sdk/compression reports an unknown stored algorithm as Unsupported.
    • files-sdk/dedup keeps size and etag on 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 Invalid instead of a retried Provider error.
    • Presigned POST uploads enforce the 7-day expiry limit.
    • An explicit amazonaws.com endpoint (or AWS_ENDPOINT_URL*) counts as AWS for events and conditional writes on both engines.
    • Backblaze B2 refuses maxSize up front, since it has no presigned POST.
    • Bun S3 reports a 401/403 on HEAD as Unauthorized instead of retrying it.
  • 51e7bd1: Report failed keys through onProgress in transfer() and sync(), so done reaches total. 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 with status: "failed" (its error is still in the result’s errors), and sync() does the same for a failed prune. TransferProgress["status"] gains "failed" alongside "transferred" and "skipped", and SyncProgress["status"] alongside "uploaded", "skipped", and "deleted"; an exhaustive switch over the status needs the new case. Under stopOnError the run still stops at the first failure, so the keys after it never settle.

  • 86ec31c: Fix the Vercel Blob adapter (files-sdk/vercel-blob) choosing BLOB_READ_WRITE_TOKEN over OIDC on Vercel Functions. When a project had both BLOB_STORE_ID and BLOB_READ_WRITE_TOKEN, the adapter saw no OIDC token in process.env (on Functions it arrives per request, in the x-vercel-oidc-token header) and passed the long-lived read-write token explicitly, so @vercel/blob never looked for the request’s token. With a store id and no token option, the adapter now always lets @vercel/blob pick the credential on each call, the way it does when called directly: the request header first, then VERCEL_OIDC_TOKEN (refreshed when expired on @vercel/blob 2.5 and later), then BLOB_READ_WRITE_TOKEN. An explicit token still wins, and an explicit oidcToken with no store id still throws. When only a store id is configured and no token turns up for a call, that operation now throws an Invalid missing credentials error, never retried, instead of @vercel/blob’s “No blob credentials found”, which becomes its cause.

  • 4a2b2b6: Fix OIDC authentication in the Vercel Blob adapter (files-sdk/vercel-blob) on Vercel Functions. There the OIDC token arrives per request, in the x-vercel-oidc-token header, rather than in process.env. With an OIDC-only store (BLOB_STORE_ID and no BLOB_READ_WRITE_TOKEN), vercelBlob() threw “missing credentials” at construction even though @vercel/blob could authenticate. The adapter now passes the store id and lets @vercel/blob find the OIDC token itself, for environment tokens too. So a fresh request-header token wins over a stale VERCEL_OIDC_TOKEN, and on @vercel/blob 2.5 and later an expired local token from vercel env pull is refreshed. An explicit oidcToken option is still used as given.

  • 3a8da5e: List every current Wasabi region in the region option’s documentation for the Wasabi adapter (files-sdk/wasabi). The list was missing us-west-2 (San Jose), eu-west-3 (United Kingdom), and eu-south-1 (Milan). The adapter already built the right endpoint for them, since every Wasabi region uses https://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, or client: "fetch" failed wrangler deploy and wrangler dev with Could 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, and files-sdk alone is enough, as the docs already said. When the packages are installed they are still resolved and bundled, so client: "aws-sdk" keeps working everywhere it did, including Workers with a DOMParser polyfill. If the "aws-sdk" engine runs without @aws-sdk/client-s3 and the two presigners, its first call now rejects with a FilesError that names them and suggests client: "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”.

Last updated on

Was this page helpful?