---
changelog:
  category: Release
  version: files-sdk@2.6.1
date: '2026-09-29T04:34:55Z'
seo:
  description: >-
    The downloadFile tool in files-sdk/ai-sdk, files-sdk/openai, and
    files-sdk/claude now enforces maxBytes on the bytes it actually reads. It
    used to check only…
title: files-sdk@2.6.1
type: changelog
---
### Patch Changes

- add46b5: The `downloadFile` tool in `files-sdk/ai-sdk`, `files-sdk/openai`, and `files-sdk/claude` now enforces `maxBytes` on the bytes it actually reads. It used to check only the size `head()` reported and then read the whole body, so an object replaced between the two calls, or a size that under-reported the body, bypassed the cap. The body is now streamed and the download is cancelled with the same `maxBytes` error as soon as it passes the limit.
- 5ed8b95: `files-sdk/api` proxied downloads now send `Accept-Ranges: none` when the adapter can't serve byte ranges, instead of always advertising `bytes`.
- 5ed8b95: `files-sdk/api` proxied downloads now honour `If-Range`. A resumed download whose validator (entity tag or date) no longer matches the object gets the whole new object with a `200`. Before, it got a `206` slice of the changed object that the client spliced onto the old bytes. Weak entity tags never satisfy `If-Range`.
- 5ed8b95: `files-sdk/api` proxied downloads now send `X-Content-Type-Options: nosniff`, so a browser won't sniff stored bytes into an executable type when a download is served inline.
- 5ed8b95: `files-sdk/api` no longer applies the cross-origin (CSRF) check to the bulk `head` request. Like `head` and `exists`, it is a read, and the `Origin` check is only for requests that change storage.
- 5ed8b95: `files-sdk/api` docs fixes. `defaultExpiresIn` is documented as what it is: the expiry used when the client doesn't ask for one. It was described as a ceiling, but a client can request a longer `expiresIn`, so cap that with `authorize`'s `maxExpiresIn`, which is now documented to cover upload URLs too. A stale module comment that said only the Next.js binding existed now lists every framework binding.
- 5ed8b95: `files-sdk/api` `purge` with no key (empty the trash) no longer deletes trash entries that an `authorize` `filterKeys` hides when there is no `keyPrefix`. It used to call the plugin's bare `purge()`, emptying the whole trash. Now it purges only the entries the caller can see, as it already did under a `keyPrefix`.
- 5ed8b95: `files-sdk/api` now bounds the work a single request can ask for. Bulk `keys[]`, presign `files[]` and `complete` `completions[]` over `maxBatchSize` (default 1000) get `413` with reason `count`. A client-supplied bulk `concurrency` is clamped to `maxConcurrency` (default 16), and a `search` page `limit` to `maxListLimit`. A JSON body over `maxJsonBodySize` (default 1 MiB) gets `413` without being buffered. All three limits are new `createFilesRouter` options.
- 5ed8b95: `files-sdk/api` now refuses `search` patterns that could tie up the server. A glob such as `*a*a*a*a*a*a*a*a*a*a*b` or a flat regex chain such as `^(.*a){10}.*b$` could take seconds to test against a single key on the backtracking regex engine, and the old check only caught nested repetition. The gateway now answers `422` before matching any key when a pattern is longer than `maxSearchPatternLength` (default 256 characters) or has more than `maxSearchWildcards` unbounded wildcards or quantifiers (default 4; an unanchored regex counts one extra). Both limits are new `createFilesRouter` options.
- 82d82e0: `files-sdk/appwrite` now uploads an empty body through a resumable (`control`) upload as a single `createFile` request. It used to send the invalid chunk header `Content-Range: bytes 0--1/0`.
- 82d82e0: `files-sdk/appwrite` `upload()` and `copy()` now overwrite an existing key instead of failing with `Conflict`. Appwrite can't update a file's content, so the existing file is deleted and created again; this isn't atomic, and file-level permissions on the old file aren't carried over.
- 82d82e0: `files-sdk/appwrite` `url()` no longer silently ignores `responseContentDisposition`. A bare `"attachment"` now returns the `/download` URL, which Appwrite serves as an attachment; any other value (a custom filename, `inline`) throws, since Appwrite has no per-request override. Ignoring it served user uploads inline even when a download was asked for.
- 5981d9d: `files-sdk/audit` now records `error.applied: true` when a conditional mutation committed at the provider and a plugin inside `audit()` then rejected the call. The core marked the error as applied only after it had left the plugin chain, so `audit()` (and any other wrapping plugin) saw a plain failure for a write that had actually landed.
- b47b9cc: `copy()` in `files-sdk/azure` now works with a `credential` and `useUserDelegationSas: false`. The copy source is authorized with the credential's bearer token, where it was previously sent unauthenticated and rejected for private containers.
- b47b9cc: Explicit auth options passed to `files-sdk/azure` (`connectionString`, `accountKey`, `credential`, `sasToken`) now always win over environment variables: `AZURE_STORAGE_CONNECTION_STRING`, `AZURE_STORAGE_ACCOUNT_KEY` / `AZURE_STORAGE_KEY`, and `AZURE_STORAGE_SAS_TOKEN` are read only when none of those options is passed. An explicit `endpoint` is also honored when a connection string carries an account key or SAS, instead of being ignored. The missing-credentials error now lists every accepted option and environment variable.
- b47b9cc: Buffered ranged downloads from `files-sdk/azure` now clamp a range `end` past the end of the blob to the blob's size, as streamed downloads and other adapters already do, instead of failing.
- b47b9cc: `files-sdk/azure` now reports `signedUrl.supported: false` when it has no signer (SAS-only, anonymous, or `credential` with `useUserDelegationSas: false`), where `url()` and `signedUploadUrl()` always throw. Previously it always reported `true`.
- b47b9cc: Signing failures in `files-sdk/azure` `url()`, such as a 403 when fetching a user delegation key, are now mapped to a `FilesError` (for example `Unauthorized`) instead of escaping as the raw Azure SDK error.
- b47b9cc: User Delegation SAS URLs from `files-sdk/azure` can no longer outlive the delegation key that signs them. With a `credential`, `url()` and `signedUploadUrl()` now throw for an `expiresIn` above 7 days instead of returning a URL that stops working when its key expires, and `files.capabilities.signedUrl.maxExpiresIn` reports the 7-day cap so the gateway clamps to it.
- 33891e7: The `files-sdk/backblaze-b2` `region` JSDoc lists `us-west-004` instead of `us-east-004`, which isn't a Backblaze B2 cluster.
- aefa047: `files-sdk/box` now lists the folder a `prefix` points into. `list()` only ever read `rootFolderId` and compared the whole prefix against child names, so `list({ prefix: "photos/", delimiter: "/" })` came back empty, the folder prefixes it returned led nowhere, and a `Files` client `prefix` listed nothing. The part of the prefix up to its last `/` now picks the folder (resolved under `rootFolderId`), the rest filters child names, and keys come back in full (`photos/cover.jpg`); a prefix into a missing folder lists nothing.
- aefa047: `files-sdk/box` now pages folder listings by marker instead of offset, in `list()` and in the key lookups every other method runs. Box rejects an `offset` above 10,000, so folders larger than that could neither be listed past that point nor have their later files found. `list()` cursors are now Box's opaque markers; numeric cursors from earlier versions are no longer accepted.
- aefa047: `files-sdk/box` now reports `capabilities.signedUrl.supported: false` when built with `publicByDefault` or `publicBaseUrl`, since `url()` returns a permanent public link in those modes rather than a signed one. The `defaultUrlExpiresIn` docs now say it is accepted for API symmetry but not honoured, because Box controls the download URL's lifetime.
- c7aa8aa: `FilesError` now matches across bundled copies of the class. The package root, the edge entries (`files-sdk/api`, `files-sdk/client`, …) and each framework binding bundle their own copy, so `instanceof FilesError` failed whenever an error crossed entry points: a `files-sdk/api` gateway answered 500 `Provider` for every `FilesError` raised by `Files` (a missing key returned 500 instead of 404) or thrown from `authorize` (500 instead of 401), and errors from `files-sdk/client` or `useFiles` did not match the `FilesError` imported from `files-sdk`. Resolves #164.
- 33f9f2e: `files-sdk/bun-s3` now supports `list({ delimiter })`. Bun's list forwards the delimiter and returns `commonPrefixes`, which the adapter now surfaces as `prefixes`, and it declares `supportsDelimiter`, so folder listing no longer throws.
- 33f9f2e: `files-sdk/bun-s3` now rejects a presigned `url()` or `signedUploadUrl()` with an `expiresIn` over 604800 seconds (7 days, the SigV4 limit), with the same permanent `Provider` error as the rest of the S3 family. Bun signed longer lifetimes without complaint, but the server rejected the URL when it was used. The adapter also declares `capabilities.signedUrl.maxExpiresIn: 604800`, so the `useFiles` gateway clamps expiry to it.
- 33f9f2e: Range downloads on `files-sdk/bun-s3` work on real Bun again. Bun's `S3Stats` exposes its fields as getters, so spreading it copied nothing, `lastModified` came back `undefined`, and every ranged `download()` failed. The adapter now copies each field explicitly.
- 33f9f2e: `signedUploadUrl({ contentType })` on `files-sdk/bun-s3` now rejects. Bun's presigned PUT URLs sign only the `host` header (its `type` option becomes a `response-content-type` query parameter), so the Content-Type was never enforced and the returned header was advisory. Omit `contentType`, or use `files-sdk/s3` or `files-sdk/s3-fetch`, which sign it. The `useFiles` gateway falls back to proxying such uploads through your server.
- 82d82e0: `files-sdk/bunny-storage` `delete()` no longer reports success when the Storage API rejects the delete. The SDK's `file.remove()` resolves `false` instead of throwing on 401, 403 and 5xx responses, and the adapter ignored it. A `false` result now probes the key: a missing key is still an idempotent no-op, a probe error surfaces, and a file that still exists throws.
- fd0bd11: `cache()` (`files-sdk/cache`) now hands out copies of cached download bytes and metadata. Previously a cache hit returned the cached buffer and `metadata` object by reference, and `stream()` enqueued the cached buffer itself, so a caller mutating a chunk or a `metadata` field changed what every later hit returned.
- fd0bd11: `cache()` (`files-sdk/cache`) now caps a cached `url()` at the lifetime the URL itself states (`X-Amz-Expires` on S3 and S3-compatible adapters, `X-Goog-Expires` on GCS) when that is shorter than the requested `expiresIn`. Before, a `signedUrlPolicy({ maxExpiresIn: 60 })` placed after `cache()` could leave a 60-second URL cached for up to an hour. The `files-sdk/cache` and `files-sdk/signed-url-policy` docs now say to place `signedUrlPolicy()` before `cache()`, and the `cache()` docs list the `defaultUrlExpiresIn` option.
- add46b5: `files-sdk/claude` no longer lets approval-gated writes skip approval. `createClaudeFileTools()` listed every tool in `allowedTools`, which the Claude Agent SDK runs without consulting `canUseTool`, so `uploadFile`, `deleteFile`, `copyFile`, and `signUploadUrl` ran unprompted even under the default `requireApproval: true`; `allowedTools` now lists only the tools that need no approval. The bundled `canUseTool` also denies tools that aren't on its own MCP server (such as `Bash`, `Write`, or another server's tools) instead of allowing them; compose your own callback to authorize those.
- add46b5: The `copyFile` tool from `files-sdk/claude` (`claudeCopyFile()` and `createClaudeFileTools()`) is now annotated `destructiveHint: true`, since copying onto an existing destination overwrites it. It stays `idempotentHint: true`.
- 3b64569: Corrected the `files` CLI help text. The program description now states the actual number of supported providers (computed from the provider registry) instead of "30+", and `--dry-run` now says it writes nothing rather than claiming it makes no network calls, since `sync --dry-run` still lists both providers to build its plan.
- 8bc864a: The `files` CLI's configuration hints now name options the adapters actually read. The `--config-json` examples for Appwrite (`key`, `bucket`), Google Drive (`oauth: {…}`, `rootFolderId`), Box (`oauth`, `ccg`, `jwt` or `developerToken`), OneDrive and SharePoint (`clientCredentials: {…}`) replace flat keys that were ignored, and an Oracle Cloud error for a missing tenancy namespace now explains that it goes in `--config-json`. The registry also records `--region`, not `--endpoint`, as the required flag for Akamai, IBM Cloud Object Storage and Oracle Cloud.
- 8bc864a: `files --provider supabase` now passes `--public-base-url` through to the adapter, so `url()` returns `<publicBaseUrl>/<key>` as the flag's help text describes. Previously the flag was silently dropped for Supabase.
- 3b64569: The `files` CLI and `files-sdk/loader` now reject `--provider` names like `toString` or `constructor` with the usual "unknown provider" error. They matched inherited object properties and crashed with "entry.load is not a function".
- 3b64569: `files upload <key>` in the `files` CLI now infers the content type from the key's extension when `--content-type` isn't given, as `upload --dir` already did per file. Single-key uploads previously stored `application/octet-stream` for everything. `--dry-run` echoes the resolved type.
- 4313433: `files-sdk/client` keyless `upload(file, { contentType })` now honours `contentType`. It used to presign with the file's own `type` and ignore the option.
- 4313433: `files-sdk/client` `download(key, { range })` now rejects when the response isn't `206 Partial Content`. A gateway or storage host that ignores `Range` answers `200` with the whole object, which was returned as if it were the requested slice. This matches the server SDK.
- 4313433: Upload state in `files-sdk/client`, `files-sdk/react`, `files-sdk/vue` and `files-sdk/svelte` now always settles. Each file's `FileUploadState` is one object for the whole upload, and it ends `"success"` (keyless and keyed), `"error"` with `state.error` set, or `"aborted"`. `onProgress` fires once more after the terminal status, and bulk `upload([...])` now accepts `onProgress` with one state per item. In the hooks, `uploads` now accumulates one entry per file across every `upload()` call instead of showing only the latest one, and `progress` covers those entries. `reset()` clears the finished entries without zeroing `isUploading` while uploads are still running.
- 24a0f92: `files-sdk/cloudinary` now uploads an empty body through a resumable (`control`) upload as a single-shot request. It used to send the invalid chunk header `Content-Range: bytes 0--1/0`.
- 24a0f92: `files-sdk/cloudinary` now reads `private` and `authenticated` assets through a signed `private_download_url`. `download()` and the lazy bodies from `head()` and `list()` used to fetch the unsigned delivery URL, which Cloudinary answers with 401 for those types, so every read failed and was retried as a `Provider` error. An asset with no stored format now fails once with a non-retryable error, matching `url()`.
- 24a0f92: `files-sdk/cloudinary` now signs and sends the adapter's delivery `type` on resumable (`control`) uploads and in `signedUploadUrl()` fields. With `type: "private"` or `"authenticated"`, those uploads previously landed as public `upload` assets that the adapter's own `head()`, `list()`, and `download()` could not find.
- 24a0f92: `files-sdk/cloudinary`'s `signedUploadUrl()` now fails closed on constraints it cannot enforce: a positive `minSize` throws (like `maxSize` already did), and `contentType` throws instead of being signed as a `content_type` field that Cloudinary ignores. Omit `contentType`, or restrict formats with an upload preset's `allowed_formats`.
- ac6a6b0: `files.capabilities` now reports `signedUrl.supported: false` and `rangeRead: false` when `files-sdk/compression` is installed, matching what the plugin refuses. The `files-sdk/api` gateway picks its download path from these flags, so with the default `downloadMode: "auto"` over a signing adapter it now streams downloads through the instance instead of answering 500, and a `Range` request gets the non-range treatment (416, or the full body with `onUnsupportedRange: "ignore"`) instead of a 500.
- ac6a6b0: `head()` and `list()` results under `files-sdk/compression` now return the original bytes from their body accessors. They already reported the uncompressed `size`, but `text()`, `arrayBuffer()`, `blob()` and `stream()` returned the stored compressed bytes; they now lazily download the object back through the plugin and decompress it.
- ac6a6b0: `files-sdk/compression` now refuses resumable uploads: an `upload()` with a `control` throws a permanent `FilesError` before any I/O, and `files.capabilities` reports `multipart: false`. Compressed output isn't byte-for-byte stable across runtimes or versions (Node and Bun differ for the same input), so a session resumed in another process could splice two different compressed streams into an object that can't be decompressed. `multipart: true` without a `control` still works.
- 8dcd641: `contentType()` (`files-sdk/content-type`) no longer rejects or relabels legitimate files it used to misread. A real `favicon.ico` is accepted in `onMismatch: "reject"` mode (its registered `image/vnd.microsoft.icon` type now agrees with the sniffed `image/x-icon`); RSS, Atom, KML, GPX, XHTML and other `*+xml` files behind an `<?xml` prolog keep their declared type instead of being rejected or flattened to `application/xml`; and a leading `<!-- … -->` comment is skipped, so an SVG or XML file that opens with one is no longer labelled `text/html`. `detectContentType()` likewise reports what follows leading comments (for example `image/svg+xml`) rather than always `text/html`.
- 8dcd641: `contentType()` (`files-sdk/content-type`) now stores the type it confirmed. When the bytes agree with the type the key's extension implies, that type is forwarded to the adapter, so a `photo.png` of real PNG bytes is stored as `image/png` rather than the adapter's default (`application/octet-stream`), matching how a mismatched upload was already relabeled. Bodies it can't identify still keep an explicit or `Blob` type and otherwise the adapter's default; the key's extension alone is never stored.
- 5981d9d: Bulk `download([...])` now validates a byte range that a plugin injects into an item, exactly like a single `download()`. A malformed range, or any range on an adapter without range support, is reported as that item's error instead of being passed to the adapter, which could silently return the whole object.
- 5981d9d: Corrected several `files-sdk` type-definition docs: `timeout` applies per attempt, bulk uploads never retry (but plugin re-routed sub-operations do and fire `onRetry`), the per-adapter support lists for `range`, `delimiter`, `metadata`, `cacheControl`, and `control`, the `SignedUrlCapability` and `SignUploadOptions.maxSize` examples, the `url()` note on key encoding (public URLs percent-encode each key segment), and the `sync()` progress ordering.
- 5981d9d: Plugins can now narrow what `files.capabilities` advertises through an optional `capabilities(caps)` hook on `FilesPlugin`. The `Files#capabilities` getter folds every installed plugin's hook over the adapter-derived snapshot in `plugins` order (read-only clones from `files.readonly()` keep the narrowing), so a plugin that can't honor presigned URLs or byte ranges end to end can say so up front instead of only failing at call time. The snapshot's `signedUrl` is now a copy, so a hook can't rewrite the adapter's own declaration.
- 5981d9d: A `Files` instance with a `prefix` now rejects a key made only of slashes (`"/"`, `"//"`) with the usual "key must be a non-empty string" error. Before, the leading slashes were stripped to an empty key, so `upload("/", body)` wrote the prefix's own `prefix/` folder-marker object.
- ac6a6b0: `files.capabilities` now reports `signedUrl.supported: false` and every `conditional` flag as `false` when `files-sdk/dedup` is installed, matching what the plugin refuses; `rangeRead` still follows the adapter. The `files-sdk/api` gateway picks its download path from these flags, so with the default `downloadMode: "auto"` over a signing adapter it now streams downloads through the instance (with `Range` support) instead of answering 500.
- ac6a6b0: `files-sdk/dedup` now reports the content's SHA-256 hash as the `etag` of `upload()`, `download()`, `head()` and `list()` results. A pointer's own ETag was identical for every key and never changed with the content, so `sync()` in its default `"etag"` mode skipped same-size edits between dedup instances (leaving the destination stale) and `versioning()` ids could collide.
- ac6a6b0: `head()` and `list()` results under `files-sdk/dedup` now return the content from their body accessors instead of the empty pointer object. `text()`, `arrayBuffer()`, `blob()` and `stream()` lazily read the content-addressed blob, and `stream()` streams it rather than buffering.
- ac6a6b0: `files-sdk/dedup` no longer forwards `control` and `multipart` to the pointer write. An `UploadControl` drives exactly one upload, so passing one to `upload()` threw on the pointer write after the blob had landed; `control` and `multipart` now apply to the blob write only, and when the content is already stored no bytes move and the control is left undriven.
- ac6a6b0: `files-sdk/dedup` now refuses writes into its content store through the instance: an `upload()`, `signedUploadUrl()`, or a `copy()` / `move()` whose destination is inside the store prefix (`.dedup/` by default) throws a permanent `FilesError`. Previously any caller who could write a key could overwrite `.dedup/<sha256>` and change what every pointer to that content returned. Reads of the store and deleting a blob (for a garbage-collection sweep) still pass through.
- d4884f0: `files-sdk/dropbox` now maps Dropbox's `no_write_permission` error to `Unauthorized` instead of `Conflict`.
- d4884f0: `files-sdk/dropbox`'s `url()` now applies the 4-hour `expiresIn` cap only to temporary links. With `publicByDefault` or `publicBaseUrl`, a longer `expiresIn` used to throw with advice to set `publicByDefault` even when it was set. In those permanent-link modes `capabilities.signedUrl` now reports `supported: false` with no `maxExpiresIn`.
- ac6a6b0: `files.capabilities` now reports `signedUrl.supported: false` and `rangeRead: false` when `files-sdk/encryption` is installed, matching what the plugin refuses. The `files-sdk/api` gateway picks its download path from these flags, so with the default `downloadMode: "auto"` over a signing adapter it now streams downloads through the instance instead of answering 500, and a `Range` request gets the non-range treatment (416, or the full body with `onUnsupportedRange: "ignore"`) instead of a 500.
- ac6a6b0: `head()` and `list()` results under `files-sdk/encryption` now return plaintext from their body accessors. They already reported the plaintext `size`, but `text()`, `arrayBuffer()`, `blob()` and `stream()` returned the stored ciphertext; they now lazily download the object back through the plugin and decrypt it, so a `cache()` head hit and a miss return the same bytes.
- ac6a6b0: `files-sdk/encryption` now refuses resumable uploads: an `upload()` with a `control` throws a permanent `FilesError` before any I/O, and `files.capabilities` reports `multipart: false`. Each upload encrypts under a fresh random data key, so a session resumed in another process spliced parts of two different ciphertexts into an object that could never be decrypted. `multipart: true` without a `control` still works.
- 5981d9d: `files-sdk/failover` no longer fails over on `permanent` errors by default. The core SDK's pre-I/O rejections (an invalid key, or an option the adapter can't honor such as `metadata`, `cacheControl`, `range`, `delimiter`, or `control`) are now `FilesError`s with `permanent: true`, so an upload with metadata to a primary without metadata support throws instead of silently landing on a secondary. The failover docs no longer point to a `replication()` plugin that doesn't exist.
- 4279df6: `files-sdk/fastify` JSDoc example now calls `app.removeAllContentTypeParsers()` before adding the catch-all parser, matching the docs. Fastify's built-in `application/json` and `text/plain` parsers take precedence over `*` and would otherwise consume the body the gateway needs.
- b47b9cc: `files-sdk/firebase-storage` no longer lets `GOOGLE_APPLICATION_CREDENTIALS` override explicit `credentials`, and it now reads that variable through Application Default Credentials instead of `cert()`. Explicit `serviceAccountPath` and `credentials` options now always win over environment variables, and workload identity federation (`external_account`) and `authorized_user` credential files work instead of crashing at construction.
- 244926c: `files-sdk/fs` `list()` no longer stops walking when a subdirectory is removed mid-listing. An `ENOENT` from any nested directory ended the whole walk, silently dropping every directory not yet visited; that directory is now skipped, and only a missing root lists as empty.
- 244926c: `files-sdk/ftp` now maps connect and login failures like every other error, so a rejected login (530) is `Unauthorized` instead of a retryable `Provider` error that the client kept retrying. A failed connect or login also closes its control socket, which used to leak once per attempt.
- 244926c: `files-sdk/ftp` `delete()` and `delete([...])` no longer report success when the server refuses the delete. They passed basic-ftp's ignore-errors flag, which swallows every FTP error reply, so "550 Permission denied", "450 file busy" and "530 not logged in" all looked like a successful delete. Only a file that is really gone is now a no-op: after a 550 the adapter checks the parent listing, and a file that is still there throws `Unauthorized`; other error replies throw as usual.
- 244926c: `files-sdk/ftp` `list()` no longer comes back empty when a subdirectory disappears (or answers "not found") partway through the recursive walk: that directory is skipped and the rest is listed, while a missing root still lists as empty. A 450 reply ("file busy") is now a retryable `Provider` error rather than `NotFound`.
- 244926c: `files-sdk/ftp` resumable uploads (`upload({ control })`) now append to a `<key>.fls-part` staging file and rename it over the key when the upload completes. They used to write straight to the key: starting an upload deleted the existing file, a paused or crashed upload left a truncated file readable under the key, and `abort()` deleted the key. `list()` hides staging files, and writes to keys ending in `.fls-part` now throw.
- d4884f0: `files-sdk/google-drive` now rejects keys, metadata entries, content types, and `cacheControl` values that overflow Drive's 124-byte limit per `appProperties` entry (key and value, UTF-8) with a non-retryable error before any Drive call. Keys can be at most 117 bytes. These writes used to fail with a 400 that was retried as a transient `Provider` error.
- d4884f0: `files-sdk/google-drive` now restores literal `\n` escapes in `GOOGLE_DRIVE_PRIVATE_KEY` to newlines, as `files-sdk/firebase-storage` already does, so a PEM key pasted into an env file or CI secret parses.
- d4884f0: `files-sdk/google-drive` now maps Drive's `rateLimitExceeded` and `userRateLimitExceeded` 403 responses to a retryable `Provider` error instead of `Unauthorized`, so they are retried with backoff as Google recommends. Other 403s still map to `Unauthorized`.
- d4884f0: The `rootFolderId` docs in `files-sdk/google-drive` now describe the actual fallback order: `GOOGLE_DRIVE_ROOT_FOLDER_ID`, then `driveId`, then `"root"`.
- d4884f0: `files-sdk/google-drive`'s `signedUploadUrl()` on an existing key now clears the previous upload's stored content type, `cacheControl`, and metadata. Drive merges `appProperties` on update, so `head()` kept reporting the old content type after the new bytes landed.
- 4279df6: `files-sdk/koa` now passes the gateway the pre-mount URL (`ctx.originalUrl`). Under `koa-mount`, `ctx.req.url` has the mount prefix stripped, so the proxy-upload target the gateway builds pointed at the wrong path.
- 6e9580b: `files-sdk/netlify-blobs` now infers the content type on upload the same way the other adapters do: a `Blob` or `File` keeps its own `type` and a string body is stored as `text/plain; charset=utf-8`. Previously every upload without an explicit `contentType` was recorded as `application/octet-stream`, so `head()` and `download()` reported the wrong type.
- 6e9580b: `files-sdk/netlify-blobs` now classifies errors by the HTTP status on Netlify's `BlobsInternalError` instead of only parsing it from the message. When Netlify sends an `x-nf-error` header the message no longer contains the status, so 404, 401/403 and 409/412 responses were reported as `Provider` errors rather than `NotFound`, `Unauthorized` and `Conflict`.
- 6e9580b: `files-sdk/netlify-blobs` `list()` now returns a `cursor` when `limit` leaves entries unreturned and resumes from it, so `listAll({ limit })` and paginated file browsers see every key instead of silently stopping after the first page. The cursor is the last key of the page (Netlify's own cursor is internal to its SDK), and with `delimiter` a folder counts against `limit` like a file, matching the other key-list adapters.
- 4279df6: `files-sdk/nitro` now serves requests Nitro handles in-process, such as the edge preset's `localFetch` and an SSR `$fetch` to your own API. They used to fail with a `500`, because the mock request Nitro builds has no socket and keeps its body on the event. The binding now uses the event's Web `Request` when h3 provides one, and otherwise reads the body where h3 stores it.
- 4279df6: `files-sdk/nitro` no longer adds a socket `close` listener per request that is never removed. On a keep-alive connection they piled up until Node printed `MaxListenersExceededWarning`. The listener is now removed once the response finishes.
- aefa047: `files-sdk/onedrive` and `files-sdk/sharepoint` now upload an empty body through a resumable (`control`) upload with the simple `PUT /content` and drop the unused upload session. They used to send the invalid chunk header `Content-Range: bytes 0--1/0` to a Graph upload session, which needs at least one byte.
- aefa047: `files-sdk/onedrive` now reads the `ONEDRIVE_DRIVE_ID` / `ONEDRIVE_SITE_ID` / `ONEDRIVE_USER_ID` env targets only when no `driveId`, `siteId`, or `userId` option is passed. An explicit target plus an unrelated env target used to throw "pass at most one", and since `files-sdk/sharepoint` always passes the resolved `driveId`, any of those env vars broke every SharePoint adapter.
- aefa047: `files-sdk/onedrive` and `files-sdk/sharepoint` now list the folder a `prefix` points into. `list()` only ever read the root folder and compared the whole prefix against child names, so `list({ prefix: "photos/", delimiter: "/" })` came back empty, the folder prefixes it returned led nowhere, and a `Files` client `prefix` listed nothing. The part of the prefix up to its last `/` now picks the folder, the rest filters child names, and keys come back in full (`photos/cover.jpg`); a prefix into a missing folder lists nothing.
- add46b5: The `uploadFile` tool from `files-sdk/openai`'s `createAgentsFileTools()` (and `agentsUploadFile()`) now has a strict-mode-valid parameter schema. The OpenAI Agents SDK sends Zod-typed tools with `strict: true`, and the free-form `metadata` map could not be expressed in strict mode, so OpenAI rejected the tool definition. The Agents `uploadFile` tool no longer takes `metadata`, matching the Responses factory under `strict`.
- 33891e7: The `files-sdk/ovhcloud` `region` and `endpoint` JSDoc now matches OVHcloud's endpoint list. Sydney is `ap-southeast-syd` (not `syd`), and the default `s3.<region>.io.cloud.ovh.net` host is OVHcloud's main endpoint, which serves every storage class and stores objects as Standard by default. It was described as the High Performance endpoint, with `s3.<region>.cloud.ovh.net` as Standard. The legacy `perf` endpoint and the Swift-backed endpoint are now described as opt-in overrides.
- 3963e72: The published `files-sdk` package now ships its MIT `LICENSE` file (earlier tarballs had none) and declares `"engines": { "node": ">=20" }`, the minimum Node version the SDK runs on (it relies on `Array.prototype.toSorted` and `toReversed`). The package also gains npm `keywords`.
- 82d82e0: `files-sdk/pocketbase` now logs in again once the superuser token from `adminEmail`/`adminPassword` expires. The first login's promise was kept forever, so after expiry every call ran unauthenticated.
- 82d82e0: `files-sdk/pocketbase` `download()` now reports the file's content type from the file response instead of always `application/octet-stream`.
- 82d82e0: `files-sdk/pocketbase` `list({ prefix })` now returns only keys that start with the prefix exactly. PocketBase runs the `~` filter as SQL `LIKE`, so `_` and `%` in the prefix acted as wildcards and letters matched case-insensitively (`prefix: "A_"` returned `a1.txt`). The server filter still narrows the page, and the adapter now matches the prefix exactly on the results, so a page can hold fewer than `limit` items.
- 8bc864a: The `files-sdk/providers` catalog now matches what each adapter actually requires. Akamai and IBM Cloud Object Storage list `region` instead of `endpoint` (the endpoint is derived from it), Oracle Cloud lists its required `namespace`, PocketBase lists its required `collection`, `BUNNY_STORAGE_REGION` moves to required, and `FIREBASE_STORAGE_BUCKET` moves to optional because the bucket defaults to `<projectId>.firebasestorage.app`. The `s3` entry now declares the `AWS_ENDPOINT_URL_S3` / `AWS_ENDPOINT_URL` and `AWS_REQUEST_CHECKSUM_CALCULATION` / `AWS_RESPONSE_CHECKSUM_VALIDATION` variables the adapter checks, `bun-s3` lists Bun's `AWS_ENDPOINT` / `S3_ENDPOINT`, and the FTP, SFTP and WebDAV notes no longer claim `signedUploadUrl()` works with `publicBaseUrl` (it is unsupported) or that WebDAV runs on edge runtimes (it imports `node:stream`). Descriptions also now match current behaviour: Firebase Storage's `GOOGLE_APPLICATION_CREDENTIALS` accepts any Application Default Credentials file, `s3-fetch` reads `AWS_SESSION_TOKEN` only when the keys also come from the environment, WebDAV infers token auth from a `token` option, and OVHcloud is no longer described as the High Performance tier.
- e955340: `files-sdk/r2` now maps Workers binding error codes to the right `FilesError` codes, following Cloudflare's published table. Bad credentials (10002) and other auth failures (10003, 10018, 10035) are `Unauthorized`, a missing key, bucket, or upload (10007, 10006, 10024) is `NotFound`, and a failed precondition or non-empty bucket (10031, 10008) is `Conflict`. Before, 10002 mapped to `NotFound`, so `exists()` returned `false` on bad credentials, and 10007 (NoSuchKey) mapped to `Conflict`.
- e955340: The `files-sdk/r2` `binding` option now documents that an upload through a Workers binding needs a known length. Strings, bytes, and `Blob`s always work; a `ReadableStream` works only as a request or response body with a `Content-Length` or as the readable side of a `FixedLengthStream`, because workerd rejects a stream of unknown length.
- e955340: In HTTP mode, `files-sdk/r2`'s `signedUploadUrl({ maxSize })` now returns a rejected promise instead of throwing synchronously, matching binding mode and every other adapter method. This only changes direct adapter calls; `files.signedUploadUrl()` already rejected.
- ac7103b: A paused resumable upload (`upload(key, body, { control })` after `control.pause()`) now rejects when the caller's `signal` (or the constructor `signal`) aborts. Previously the parked upload ignored the signal and the `upload()` promise stayed pending until `resume()` or `control.abort()`. Like any external abort, the session is kept so the upload can still be resumed from `control.toJSON()`, and the pause gate no longer leaves an abort listener on the signal for every pause/resume cycle.
- ac7103b: Resumable uploads on `files-sdk/gcs`, `files-sdk/firebase-storage`, and `files-sdk/google-drive` now classify a failed session request by its HTTP status: 404 and 410 (an unknown or expired session) map to `NotFound`, 401/403 to `Unauthorized`, and 409/412 to `Conflict`, all flagged `permanent`. They used to surface as `Provider` errors, so each chunk against a dead session was retried until the retry budget ran out.
- e955340: `files-sdk/s3` with an explicit `endpoint` (and every S3-compatible wrapper built on it) now sets `requestChecksumCalculation` and `responseChecksumValidation` to `"WHEN_REQUIRED"`. `@aws-sdk/client-s3` 3.729 and later add an `x-amz-checksum-crc32` header to every `PutObject` and `UploadPart`, plus a checksum parameter to presigned PUT URLs, and some S3-compatible services reject it (Backblaze B2 reported "Unsupported header 'x-amz-checksum-crc32'"). Checksums are now sent only where an operation requires one; `DeleteObjects` still carries one because S3 requires it. The `AWS_REQUEST_CHECKSUM_CALCULATION` / `AWS_RESPONSE_CHECKSUM_VALIDATION` env vars still override, and canonical AWS keeps the SDK default.
- e955340: `files-sdk/s3-fetch` no longer pairs an `AWS_SESSION_TOKEN` from the environment with keys you pass explicitly. On a Lambda or SSO shell, `s3Fetch()` with static R2 or MinIO keys signed every request with the shell's unrelated token and got a 403. The env token is now read only when `accessKeyId` and `secretAccessKey` also come from the environment; an explicit `sessionToken` still applies.
- e955340: Doc-comment and message fixes in `files-sdk/s3`. The `publicBaseUrl` JSDoc now says keys are URL-encoded per segment (it wrongly said they were embedded literally), the `mapS3Error` and internal `defaultProviderMessage` notes describe how the S3-compatible wrappers actually relabel errors, and the missing-`@aws-sdk/lib-storage` error now says it's also needed for `multipart` and for `ReadableStream` bodies of unknown length, not only `onProgress`.
- e955340: Presigned URLs on the S3 family now reject an `expiresIn` over 604800 seconds (7 days, the SigV4 limit) with a clear `Provider` error on both engines. The fetch engine (`files-sdk/s3-fetch`, and the `client: "fetch"` mode of `files-sdk/r2`, `files-sdk/minio`, and `files-sdk/rustfs`) used to return a URL the server rejected, and the aws-sdk engine threw a bare "S3 error". `files-sdk/s3`, `files-sdk/s3-fetch`, every S3-compatible wrapper, and R2 hybrid signing now declare `capabilities.signedUrl.maxExpiresIn: 604800`, so the `useFiles` gateway clamps expiry to it.
- e955340: Resumable (`control`) uploads on `files-sdk/s3` and its S3-compatible wrappers now grow the part size to fit the body in S3's 10,000-part limit, up to the 5 GiB part maximum. At the 5 MiB default, bodies over about 48.8 GiB needed more than 10,000 parts, and S3 rejected part 10,001 after everything before it had uploaded. The chosen part size is pinned in the resume token.
- 244926c: `files-sdk/sftp` now maps connect failures like every other error, so a rejected login is `Unauthorized` instead of a retryable `Provider` error that the client kept retrying.
- 244926c: `files-sdk/sftp` `list()` no longer comes back empty when a subdirectory disappears partway through the recursive walk: that directory is skipped and the rest is listed, while a missing root still lists as empty.
- 244926c: `files-sdk/sftp` resumable uploads (`upload({ control })`) now append to a `<key>.fls-part` staging file and rename it over the key when the upload completes. They used to write straight to the key: starting an upload deleted the existing file, a paused or crashed upload left a truncated file readable under the key, and `abort()` deleted the key. `list()` hides staging files, and writes to keys ending in `.fls-part` now throw.
- aefa047: `files-sdk/sharepoint` now classifies Graph errors from its site and drive lookups like every other Graph error: 401/403 map to `Unauthorized` and 404 to `NotFound`. They surfaced as retryable `Provider` errors before.
- df8e277: The `files-sdk/soft-delete` docs now describe what is trashed accurately: only deletes made through the `Files` instance. Overwrites (including presigned uploads) and deletes made directly against the provider are not; use `versioning()` to keep overwritten bytes.
- 6e9580b: `files-sdk/supabase` now sends `cacheControl` the way Supabase expects it, as a number of seconds. Previously a header like `"public, max-age=60"` was stored as `max-age=public, max-age=60`. A `max-age=<n>` value (optionally with `public`) or a bare number of seconds is accepted; values Supabase can't store, such as `no-store` or `immutable`, now throw instead of being mangled.
- 6e9580b: `files-sdk/supabase` now recognizes a missing object. Supabase Storage answers with HTTP 400 and puts the real status in the body (`statusCode: "404"`, `code: "NoSuchKey"`), which used to map to a `Provider` error, so `exists()` threw instead of returning `false` and `head()`/`download()` never raised `NotFound`. The body status and code now take priority, and a malformed-key `InvalidKey` error is no longer reported as `Unauthorized`.
- 6e9580b: Resumable uploads (`upload({ control })`) on `files-sdk/supabase` now carry `cacheControl` and user `metadata` into the TUS session instead of dropping them. Chunks also stay at the 6 MiB size Supabase requires, so a `multipart.partSize` no longer breaks the upload.
- 6e9580b: `signedUploadUrl({ contentType })` on `files-sdk/supabase` now throws. Supabase signed upload URLs don't bind a Content-Type, so the header it returned was advisory only. Restrict types with the bucket's allowed MIME types, or validate at your gateway.
- 4313433: `files-sdk/svelte` `useList`, `useFile` and `useSearch` now abort their in-flight request when the last subscriber to their stores goes away. That happens when the component is destroyed or a `$:` block swaps in a new query, which matches how the React and Vue bindings abort on unmount. A synchronous read such as `get(store)` doesn't trigger it. A query aborted this way runs again when something subscribes to it.
- 5981d9d: `files.tier()` from `files-sdk/tiering` now throws `ReadOnly` on a read-only instance (`files.readonly()` or `readonly: true`) instead of moving and deleting data. `Files` exposes a new `isReadOnly` getter so plugins whose `extend` methods write through an internal instance can refuse the same way. The `tier()` docs now note that its moves bypass hooks and the instance's other plugins.
- 5981d9d: With `fallback: true`, `url()` on `files-sdk/tiering` now checks which tier holds the key and signs against that one. Presigning adapters mint a URL without checking the object exists, so a size-routed object or one moved with `tier()` got a dead link to the other tier. The docs now also describe the merged `list()` as globally key-ordered across pages.
- df8e277: The `files-sdk/tracing` docs no longer claim that inner plugins' sub-operations become child spans. With `tracing()` first, each span covers the full caller-facing operation including time spent in inner plugins, but their own sub-operations call inward past it and are not traced separately; place `tracing()` last to get one span per provider call.
- ac7103b: `transfer()` and `sync()` from `files-sdk` now drop user metadata when the destination can't store it (`dest.capabilities.metadata` is `false`), as their docs describe. Before, every metadata-bearing object failed with a "`metadata` is not supported" per-key error when copying to an adapter such as WebDAV, Dropbox, or Bunny Storage.
- ac7103b: A throwing `onProgress` callback no longer breaks `transfer()` or `sync()` from `files-sdk`. Progress is now fire-and-forget, as it is for uploads: before, a throw turned every key that had already been copied into a per-key error, and `sync({ prune: true })` rejected after it had already deleted destination keys.
- 24a0f92: `files-sdk/uploadthing`'s `signedUploadUrl()` now throws on a positive `minSize` instead of silently ignoring it, because UploadThing's ingest URLs have no minimum-size constraint. `minSize: 0` is still accepted.
- 8dcd641: The `files-sdk/validation` and `files-sdk/content-type` docs now spell out plugin order. `contentType()` must come before `validation()` in the `plugins` array: in the reverse order `validation()` approves the type the client claimed (say `image/png` from `avatar.png`) and `contentType()` then relabels the upload from its bytes (to `text/html`), slipping past `allowedTypes`. `validation()` must also come before `versioning()`, `softDelete()` and `dedup()`, whose internal writes (`.versions/…`, `.trash/…`, `.dedup/…`, empty pointer bodies) would otherwise be rejected by `key` or `minSize` rules.
- 8dcd641: `validation()` (`files-sdk/validation`) no longer reads a known-length body into memory to check `maxSize` / `minSize`. Strings, byte arrays, `Blob`s and `File`s are measured from their length and forwarded untouched, so a multi-gigabyte `File` over the limit is rejected without being buffered, and a size-only policy no longer overrides the adapter's default content type. Only unknown-length streams (and a `Blob` that reports no finite size) are still buffered to measure them.
- 8dcd641: `validation()` (`files-sdk/validation`) now stores the type it approved. With `allowedTypes` set, the checked type (an explicit `contentType`, else the `Blob`'s type, else the type implied by the key's extension) is forwarded as the upload's `contentType`. Previously an upload approved as `image/png` from its `.png` key was stored as the adapter's default, and with a size rule a string body was forced to `text/plain`.
- a6ec2bd: `files-sdk/vercel-blob` now throws when `upload({ cacheControl })` has no `max-age` directive (for example `"no-store"`). Vercel Blob only stores a cache max-age, so such values used to be silently dropped; pass a value like `"public, max-age=3600"` instead.
- a6ec2bd: `files-sdk/vercel-blob` resumable and multipart uploads (`control`, `multipart`) now send the adapter's `allowOverwrite` setting and the upload's `cacheControl`, like a plain `upload()` does. Previously a resumable upload to an existing key failed under the default `allowOverwrite: true`, and its `cacheControl` was silently dropped.
- a6ec2bd: `files-sdk/vercel-blob` now classifies `@vercel/blob` errors by their class. The SDK's errors carry no HTTP status and a generic `name`, so every failure used to surface as a `Provider` error: `exists()` on a missing key threw instead of returning `false`, and `head()`/`download()` never reported `NotFound`. Missing blobs now map to `NotFound`, access and token errors to `Unauthorized`, ETag precondition failures and uploads to an existing key under `allowOverwrite: false` to `Conflict`, and deterministic rejections (content type not allowed, file too large, missing store) are flagged `permanent` so `retries` doesn't re-send them.
- df8e277: `versioning()` (`files-sdk/versioning`) no longer snapshots on a `copy` or `move` of a key onto itself. Those change nothing, but each one used to add a version and, with `limit`, prune an older one, so a no-op could destroy history.
- df8e277: `versioning()` (`files-sdk/versioning`) now orders versions by when each snapshot was taken, not by the snapshotted object's last-modified time. A native move (fs, memory, FTP, SFTP) carries the source's older time onto the destination, so `restoreVersion()` could undo the wrong change and `limit` could prune the snapshot it had just taken. New version ids are `<taken>-<modified>-<etag>`; ids written by earlier releases still parse and sort before newer ones, and `FileVersion.lastModified` still reports the object's own last-modified time.
- 4313433: `files-sdk/vue` and `files-sdk/svelte` now mirror failures from `versions`, `restoreVersion`, `trashed`, `restoreTrashed` and `purge` into `error`, as `files-sdk/react` already did. All three bindings now also record errors thrown while iterating `listAll()` or `search()`.
- 4313433: `files-sdk/vue` and `files-sdk/svelte` now re-export the `FileVersion`, `TrashedFile` and `NativeFileRef` types, and `files-sdk/react` adds `NativeFileRef`, so all three bindings expose the same client types. The new `UploadManyCallOptions` and `UploadProgressCallback` types are exported from each binding as well.
- 33891e7: The `files-sdk/vultr` `region` docs and missing-region error now use Vultr's real cluster codes (`ewr1`, `sjc1`, `ams1`, `blr1`, `del1`, `sgp1`). The old examples (`ewr`, `sjc`, `ams`, …) produced hosts like `ewr.vultrobjects.com` that don't exist, and `lux` isn't a Vultr cluster.
- 244926c: `files-sdk/webdav` `list()` no longer comes back empty when a subcollection disappears partway through the recursive walk: that collection is skipped and the rest is listed, while a missing root still lists as empty.
- 244926c: `files-sdk/webdav` now uses token auth when a `token` is passed without `authType`, as documented. The `webdav` client inferred no auth in that case, so requests went out without an `Authorization` header.
- ac7103b: `files.zip(selection)` from `files-sdk/zip` no longer starts listing or downloading until the returned stream is first read, as documented. The stream used to pull one chunk eagerly, so creating an archive stream you never consumed still listed the selection and opened the first download. The `unzip` `maxEntries`, `maxEntrySize`, and `maxTotalSize` options are now documented in the type definitions.
