---
changelog:
  category: Release
  version: files-sdk@2.6.2
date: '2026-09-29T05:28:16Z'
seo:
  description: >-
    files-sdk/client download() now reports a failure at the storage host, after
    the gateway redirected to a signed URL, with the code its HTTP status
    implies…
title: files-sdk@2.6.2
type: changelog
---
### Patch Changes

- 3306ac0: `files-sdk/client` `download()` now reports a failure at the storage host, after the gateway redirected to a signed URL, with the code its HTTP status implies: `404` is `NotFound`, `401` and `403` are `Unauthorized`, and `409` and `412` are `Conflict`. Previously every such failure was a generic `Provider` error ("gateway responded 404"), so downloading a missing key through a signing adapter never surfaced `NotFound`.
- 3306ac0: `files-sdk/api` now passes the request's abort signal to every storage call it makes for `url`, the `download` redirect, `signed-upload-url`, `presign`, `complete`, and both upload `PUT`s. Previously only the read, list, delete, copy, and move calls and the proxied download received it, so the rest ran to completion after the client had disconnected, and an explicit-key upload could still store its object.
- 3306ac0: `files-sdk/api` now refuses a `search` whose string pattern uses `match: "regex"` with `422` when the pattern doesn't compile (`invalid search regex`) or could backtrack catastrophically (`search pattern is too complex`), the same as the `isRegex` form. Previously it failed later, inside the matcher, as a `500` `Provider` error.
- 3306ac0: `files-sdk/api` now binds each keyless-upload token to the endpoint path that minted it, and the proxy upload and `complete` steps refuse a token presented at any other path. Previously the token was bound only to the endpoint's query, so with two gateways sharing one secret (the `FILES_API_SECRET` default, as in the one-route-per-bucket setup), a token presigned at one route could be replayed against the other route's `?op=proxy` upload. That wrote bytes into the second route's bucket, outside its `keyPrefix`, even though its `authorize` and `operations` never approved an upload.
- 3306ac0: The default upload transport in `files-sdk/client` (and so in `files-sdk/react`, `files-sdk/vue` and `files-sdk/svelte`) now removes its abort listener from the caller's signal once each `XMLHttpRequest` settles. Previously a long-lived signal shared across many uploads kept a listener, and with it every finished request and its response, alive until the signal was aborted or collected.
- 97d5e1d: The `files` CLI now reports a malformed `--config-json` in the format the output flags ask for: plain text (`error (Provider): …`) under `--no-json`, with a stack trace under `--verbose`. Previously that error was raised before the output flags were read, so it always printed the JSON envelope.
- 97d5e1d: `loadFiles` from `files-sdk/loader` (and the `files` CLI) now keeps the adapter's original construction error as the `cause` of the `FilesError` it throws when it appends a provider's configuration hint. Previously the hinted error dropped it.
- 97d5e1d: The `files mcp` server's `download` tool now enforces `maxBytes` on the bytes it actually reads, streaming the body and cancelling the transfer as soon as it passes the cap. Previously the cap was checked against `head()` and then the whole body was buffered before a second check, so an object replaced between the two calls (or a backend that misreports its size) could pull an arbitrarily large body into memory. The tool's `size` field now reports the bytes returned.
- 97d5e1d: The `files` CLI now passes `--public-base-url` to the `box`, `dropbox`, and `bunny-storage` providers, and `--default-url-expires-in` to `box`, `dropbox`, `uploadthing`, and `vercel-blob`. Previously these providers silently ignored the flags, so `url()` kept the adapter's default expiry and Bunny Storage's `url()` failed for want of an origin even when `--public-base-url` was given. The `--region`, `--endpoint`, and `--public-base-url` help text now names the providers that actually read each flag.
- 97d5e1d: The `files` CLI now exits with code 2 on a usage error (an unknown flag, a missing required option or argument, an invalid choice). Previously these exited 1, the code the CLI documents for `NotFound` and for `exists` reporting a missing key, so a mistyped flag on `files exists` read as "missing". `--help` and `--version` still exit 0.
- 52f31bd: `files-sdk/azure` now maps failures when reading the body of a `head()` or `list()` result. For a blob deleted in between, `text()`, `arrayBuffer()`, and `stream()` now fail with a `FilesError` (`NotFound`) instead of the raw Azure `RestError`.
- 52f31bd: `files-sdk/azure`'s `signedUploadUrl()` now throws on a positive `minSize` instead of silently ignoring it, because a SAS has no minimum-size constraint. `minSize: 0` is still accepted.
- 52f31bd: `files-sdk/cloudinary` now maps failures when reading the body of a `head()` or `list()` result. A transport error now fails `text()`, `arrayBuffer()`, and `stream()` with a `FilesError` instead of escaping raw.
- 52f31bd: `files-sdk/cloudinary` resumable (`control`) uploads now classify a failed chunk by HTTP status, so a rejected signature (401) is an `Unauthorized` error that is not retried. Previously every failed chunk was a retryable `Provider` error.
- 52f31bd: `files-sdk/firebase-storage` now maps failures when reading the body of a `head()` or `list()` result. For an object deleted in between, `text()`, `arrayBuffer()`, and `stream()` now fail with a `FilesError` (`NotFound`) instead of the raw `ApiError`.
- 52f31bd: `files-sdk/gcs` and `files-sdk/firebase-storage` now report `capabilities.signedUrl.maxExpiresIn` as 604800 seconds (7 days). `@google-cloud/storage` rejects any V4 signed URL or POST policy that outlives 7 days, but the adapters left the cap unset, so the `files-sdk/api` gateway did not clamp to it and a longer `expiresIn` failed instead.
- 52f31bd: `files-sdk/gcs` now maps failures when reading the body of a `head()` or `list()` result. For an object deleted in between, `text()`, `arrayBuffer()`, and `stream()` now fail with a `FilesError` (`NotFound`) instead of the raw `ApiError`.
- 52f31bd: `files-sdk/netlify-blobs` now maps failures when reading the body of a `head()` or `list()` result. A store error, such as a rejected token, now fails `text()`, `arrayBuffer()`, and `stream()` with a `FilesError` instead of the raw `BlobsInternalError`.
- 52f31bd: Resuming a part-based upload (`upload({ control: UploadControl.from(token) })`) now ignores any already-uploaded part the provider reports past the body's last part, or at a size the body's slicing can't produce, and re-uploads that part where the body has one. `files-sdk/azure` lists every uncommitted block on the blob, including blocks an abandoned upload to the same key left behind, and those were committed into the resumed object, splicing stale bytes into it.
- 52f31bd: `files-sdk/supabase` resumable (`control`) uploads now classify TUS failures by HTTP status: 401/403 are `Unauthorized`, an expired or terminated upload (404/410) is `NotFound`, and an offset mismatch (409) is `Conflict`, none of which are retried. Previously every failure was a retryable `Provider` error, so a rejected key or an expired session was re-sent chunk by chunk before failing with the wrong code.
- 52f31bd: `files-sdk/supabase`'s `signedUploadUrl()` now throws on a positive `minSize` instead of silently ignoring it, because Supabase signed upload URLs have no minimum-size constraint. `minSize: 0` is still accepted.
- 52f31bd: `files-sdk/uploadthing` now maps failures when reading the body of a `head()` or `list()` result. A transport error, or a signing error for a private file, now fails `text()`, `arrayBuffer()`, and `stream()` with a `FilesError` instead of escaping raw.
- 52f31bd: `files-sdk/uploadthing` with `acl: "private"` now reports the 7-day `capabilities.signedUrl.maxExpiresIn` cap that `generateSignedURL` enforces, so the `files-sdk/api` gateway clamps a longer requested expiry instead of failing the call.
- 52f31bd: `files-sdk/vercel-blob` now maps failures when reading the body of a `head()` or `list()` result. A `blob.get` error in private mode, or a transport error in public mode, now fails `text()`, `arrayBuffer()`, and `stream()` with a `FilesError` instead of escaping raw.
- 52f31bd: `files-sdk/vercel-blob` resumable uploads now slice a resumed upload on the part size pinned in the `UploadControl` token, as the S3 and Azure adapters do. A resume with a different (or default) `multipart.partSize` used the new size, which misaligned with the parts the token already held.
- 3dd7d7e: Corrected editor docs across `files-sdk`: `list({ delimiter })` now lists Bun's S3 as supported (it no longer throws), `upload({ multipart })` notes that the S3 `fetch` client throws rather than ignoring it, `delete(keys, { concurrency })` names every adapter with a native bulk delete and explains that a wrapping plugin makes the per-key fan-out (and so `concurrency`) apply everywhere, `move()` lists every adapter with a native rename, `capabilities.signedUrl` names the `fs` adapter's `urlBaseUrl` option correctly, and `files-sdk/zip` states its real limit of 65,534 entries (65,535 is the ZIP64 marker the writer refuses).
- 3dd7d7e: Content types the SDK infers from a key's extension (S3-family `list()` items, FTP, SFTP, WebDAV, Dropbox, Box, `files-sdk/content-type`, `files-sdk/validation`, `files-sdk/zip`'s `unzip()`, and the CLI) now fall back to `application/octet-stream` for a root-level key with no extension or a root dotfile. Previously a bare key named after an extension, such as `json` or `html`, or a dotfile such as `.html`, was typed as `application/json` or `text/html`, while the same name inside a folder was not.
- 3dd7d7e: `files-sdk/webdav`, `files-sdk/ftp`, and `files-sdk/sftp` now reject a key that resolves to the configured root itself (`"/"`, `"."`, `"./"`) before any server call. Previously such a key addressed the root directory: on WebDAV, `files.delete("/")` sent a `DELETE` for the root collection, which removes everything under it. Their key-validation errors (traversal, null bytes, the reserved `.fls-part` suffix) are now also marked permanent, so `retries` no longer re-sends a call that can only fail the same way.
- 3dd7d7e: `files-sdk` now keeps `upload({ onProgress })` fire-and-forget on adapters that report progress themselves (S3 and the S3-compatible adapters, Azure, GCS, Firebase Storage, Vercel Blob, FTP). Previously a throwing `onProgress` on those adapters rejected the upload attempt, and with `retries` set the SDK re-uploaded the whole body on each retry before failing.
- 3dd7d7e: `files-sdk/zip`'s `unzip()` now reports an entry that inflates past its declared size as a corrupt archive. Previously it blamed "the configured unzip size limit", even though the entry was far under `maxEntrySize` and the archive's own header was what lied.
- 3dd7d7e: `files-sdk/zip`'s `unzip()` now rejects an archive that lists two entries with the same path, as its docs promise. Previously it extracted both, so the second silently overwrote the first.
- fd236a0: `files-sdk/box` now classifies Box API errors by their HTTP status again: 404 maps to `NotFound`, 401/403 to `Unauthorized`, and 409/412 to `Conflict`. The Box SDK's ESM build, which is what an `import` loads, drops `responseInfo` from every `BoxApiError`, so each one surfaced as a retryable `Provider` error. The status is now read from the error message the SDK formats. Where `responseInfo` is intact, the adapter now reads the raw Box error code from the response body, since the copy in `responseInfo.code` is JSON-quoted and never matched. A rejected OAuth grant (`invalid_grant`, `invalid_client`) now maps to `Unauthorized`.
- fd236a0: The `defaultUrlExpiresIn` JSDoc in `files-sdk/dropbox` now says that a default above 14400 seconds is capped to Dropbox's 4-hour link lifetime. It used to say such values throw, but only a per-call `expiresIn` above 14400 throws.
- fd236a0: `files-sdk/dropbox` now lists the folder a `prefix` points into when `list()` is called with a `delimiter`. The whole prefix used to be read as a folder path, so a prefix without a trailing `/` came back wrong: `photos/20` listed nothing instead of `photos/2023/` and `photos/2024/`, and `photos/2024` returned that folder's children instead of the common prefix `photos/2024/`. The part of the prefix up to its last `/` now picks the folder and the whole prefix filters its children, matching the other adapters.
- fd236a0: `files-sdk/dropbox` now treats a throttled (429) or failing (5xx) token endpoint during a refresh-token exchange as a retryable `Provider` error. It used to throw `Unauthorized`, so a transient outage of Dropbox's OAuth endpoint was never retried. A rejected refresh token still maps to `Unauthorized`.
- fd236a0: `files-sdk/google-drive` now maps a rejected OAuth grant (a revoked refresh token, a bad or deleted service-account key, reported as `invalid_grant` / `invalid_client`) to `Unauthorized` instead of a retryable `Provider` error. A failed resumable-session initiation, for `signedUploadUrl()` or a resumable `upload()`, is now classified like any other Drive error. A 401 maps to `Unauthorized`, a missing file to `NotFound`, and a rate-limited 403 stays a retryable `Provider` error. Resumable uploads used to report every initiation failure as `Provider`, and `signedUploadUrl()` treated a rate-limited 403 as `Unauthorized`.
- fd236a0: `files-sdk/google-drive` built with `publicByDefault` now grants the `anyone, reader` permission in `url()` before returning the link. Only a plain `upload()` used to grant it, so after `copy()`, a resumable upload, or a client upload through `signedUploadUrl()`, `url()` returned a link that asked for a Google sign-in.
- fd236a0: `files-sdk/box`, `files-sdk/dropbox`, `files-sdk/google-drive`, `files-sdk/onedrive`, and `files-sdk/sharepoint` now map failures when reading the body of a `head()` or `list()` result. If the file was deleted in between, `text()`, `arrayBuffer()`, and `stream()` now fail with a `FilesError` (`NotFound`) instead of the raw SDK error.
- fd236a0: `files-sdk/onedrive` and `files-sdk/sharepoint` now report rejected credentials as `Unauthorized`. The Graph client strips an auth failure down to its error name, so a revoked `oauth` refresh token or a bad `clientCredentials` secret surfaced as a retryable `Provider` error. A throttled (429) or failing (5xx) token endpoint during an `oauth` refresh now stays a retryable `Provider` error instead of `Unauthorized`.
- fd236a0: `files-sdk/onedrive` and `files-sdk/sharepoint` now report a key that names a folder as missing. `exists()` returned `true` for a folder path and `head()` returned the folder's metadata as if it were a file. They now return `false` and throw `NotFound`, as the Dropbox and Box adapters do.
- fd236a0: `files-sdk/onedrive` and `files-sdk/sharepoint` now reject `signedUploadUrl({ contentType })` before creating an upload session. A Graph upload session doesn't bind a Content-Type, so the `Content-Type` header the adapter used to return was only advisory, which the `signedUploadUrl` contract rules out. Omit `contentType`, or validate it at your gateway before issuing the URL.
- fd236a0: `files-sdk/sharepoint` now lets an explicit site or library option win over the `SHAREPOINT_*` env targets. `SHAREPOINT_DRIVE_ID` used to override an explicit `siteId`, `siteUrl`, `hostname`, `sitePath`, or `documentLibrary`. `SHAREPOINT_SITE_ID` overrode an explicit `siteUrl` or `hostname`, and `SHAREPOINT_SITE_URL` overrode an explicit `hostname`. In each case the adapter silently read and wrote a different site or library than the one passed. The env targets now apply only when no option names a site, and for the drive, no library either. An explicit `documentLibrary` still resolves against an env site, and `SHAREPOINT_HOSTNAME` still pairs with an explicit `sitePath`.
- bcb88dc: `files-sdk/appwrite` resumable uploads now classify a failed chunk by its HTTP status (`409` as `Conflict`, `401`/`403` as `Unauthorized`, `404` as `NotFound`). Previously every chunk failure was a generic `Provider` error, so a chunked upload onto an existing file ID was retried with backoff instead of failing with `Conflict`.
- bcb88dc: `files-sdk/appwrite` now rejects keys that aren't valid Appwrite file IDs on every method (`download`, `head`, `exists`, `delete`, `url`, and the `copy` source), not only on writes. Previously a key such as `..` or `.` reached node-appwrite, which leaves dots unencoded, so URL normalization pointed the request at the bucket instead of a file: `delete("..")` sent `DELETE /storage/buckets/{bucketId}`.
- bcb88dc: `files-sdk/appwrite` resumable uploads no longer delete an existing file when the upload is aborted before any of its chunks landed. Previously `control.abort()` always deleted the file at the key, so aborting after Appwrite refused a chunked upload onto an existing file ID (or before the first chunk finished) deleted that existing file.
- bcb88dc: `files-sdk/bunny-storage` now rejects keys containing `.` or `..` path segments (including their `%2e` spellings) with a `Provider` error before calling the Storage API. Previously the Bunny SDK's URL handling resolved those segments away, so `delete(".")` or `delete("a/..")` targeted the storage zone root and `delete("x/.")` the directory `x/` instead of an object.
- bcb88dc: `files-sdk/fs` `copy()` now stages the copied body (and its sidecar) next to the destination and renames it into place, like `upload()`. Previously it wrote straight to the destination path, so a symlink already sitting at the destination key or its `.meta.json` sidecar was followed and the file it pointed at, even one outside the adapter root, was overwritten; a crash mid-copy could also leave a truncated body or sidecar behind.
- bcb88dc: `files-sdk/fs`, `files-sdk/appwrite`, and `files-sdk/bunny-storage` now mark their key-validation errors as permanent. These are keys that escape the adapter root or resolve onto it, reserved sidecar names, invalid Appwrite file IDs, and Bunny keys with `.` or `..` segments. Previously these deterministic `Provider` errors counted as retryable, so a call made with `retries` re-sent the same invalid key, with backoff, until the retry budget ran out.
- bcb88dc: `files-sdk/fs` resumable uploads now classify filesystem errors from chunk writes like every other method (`ENOENT`/`ENOTDIR` as `NotFound`, `EACCES`/`EPERM` as `Unauthorized`). Previously a failed chunk write surfaced as a generic `Provider` error, so a permission failure was retried as if it were transient.
- bcb88dc: `files-sdk/ftp` and `files-sdk/sftp` streaming downloads (`as: "stream"`) now remove their abort listener once the stream ends, errors, or closes. Previously the listener stayed on the signal, so a long-lived signal such as the `Files` constructor's `signal` kept every streamed download's connection and stream in memory for as long as the signal lived.
- bcb88dc: `files-sdk/pocketbase` `copy()` now overwrites an existing destination key by replacing the file on its record, the same way `upload()` does. Previously it always created a new record, which the collection's unique key index refused, so copying onto an existing key failed with a `Provider` error.
- bcb88dc: `files-sdk/pocketbase` `download()` now maps a `401` or `403` from the file endpoint to `Unauthorized`. Previously anything other than a `404` was a `Provider` error, so a refused file token was retried as if it were transient.
- 3306ac0: The `files-sdk/api` gateway and `files-sdk/signed-url-policy` no longer accept a requested `attachment` disposition that contains control characters. Previously any value starting with `attachment` was kept as "already safe", so a caller could send `attachment\r\n…` and have it ride into the signed URL's `response-content-disposition`. Such values are now replaced with the configured default disposition, like `inline` is. A tab (`\t`) is still allowed.
- 30d0a2b: `list()` items under `files-sdk/compression` now return the original bytes from their body accessors on adapters whose listing carries no object metadata, such as S3 and the S3-compatible adapters. Without the algorithm marker in the listing the plugin passed those items through untouched, so `text()`, `arrayBuffer()`, `blob()` and `stream()` returned the stored compressed bytes; they now download the object back through the plugin and decompress it. Such items still report the stored size, since the original size lives in metadata the listing doesn't include.
- 30d0a2b: `files-sdk/compression` now marks its refusals and read failures as `permanent`: `url()`, `signedUploadUrl()`, a ranged `download()`, an unknown stored algorithm, and a failed decompress. Previously they were ordinary `Provider` errors, so a `failover()` placed before the plugin treated them as an outage and re-sent the call to its secondary, which runs without the plugin and minted an upload URL that skips compression or served raw stored bytes.
- 30d0a2b: `files-sdk/content-type` now marks its `onMismatch: "reject"` and `onUnknown: "reject"` rejections, and its `signedUploadUrl()` refusal, as `permanent`. Previously they were ordinary `Provider` errors, so a `failover()` placed before the plugin treated a rejected upload as an outage and re-sent it to its secondary, which runs without the plugin, storing the mislabeled upload anyway.
- 30d0a2b: `list()` items under `files-sdk/dedup` now return the content from their body accessors on adapters whose listing carries no object metadata, such as S3 and the S3-compatible adapters. Without the pointer marker in the listing the plugin passed those items through untouched, so `text()`, `arrayBuffer()`, `blob()` and `stream()` returned the empty pointer; they now follow the pointer to the stored content. Such items still report the pointer's own size and ETag, since the content size and hash live in metadata the listing doesn't include.
- 30d0a2b: `files-sdk/dedup` now marks its `url()` and `signedUploadUrl()` refusals as `permanent`. Previously they were ordinary `Provider` errors, so a `failover()` placed before the plugin treated them as an outage and re-sent the call to its secondary, which runs without the plugin and minted a URL that bypasses content-addressing.
- 30d0a2b: The `files-sdk/encryption` JSDoc now says to place `failover()` and `tiering()` after `encryption()`, and that only the body is encrypted. It previously said to put encryption last in the plugin array, which, combined with either of those plugins, stores whatever they route to a secondary or cold backend unencrypted; and its threat model didn't mention that the key, `contentType`, and user `metadata` are stored as-is.
- 30d0a2b: `list()` items under `files-sdk/encryption` now return plaintext from their body accessors on adapters whose listing carries no object metadata, such as S3 and the S3-compatible adapters. Without the envelope marker in the listing the plugin passed those items through untouched, so `text()`, `arrayBuffer()`, `blob()` and `stream()` returned the stored ciphertext; they now download the object back through the plugin and decrypt it. Such items still report the stored size, since the plaintext size lives in metadata the listing doesn't include.
- 30d0a2b: `files-sdk/encryption` now marks its refusals and read failures as `permanent`: `url()`, `signedUploadUrl()`, a ranged `download()`, a failed decrypt, a tampered envelope size, and a raw key of the wrong length. Previously they were ordinary `Provider` errors, so a `failover()` placed before the plugin treated them as an outage and re-sent the call to its secondary, which runs without the plugin and minted an upload URL for unencrypted bytes, signed a link to ciphertext, or served raw stored bytes.
- 30d0a2b: `files.capabilities` now reports every `conditional` flag as `false` when `files-sdk/failover` is installed, matching the plugin's refusal of every conditional operation. Previously it kept advertising the primary adapter's native support, so callers that branched on the snapshot planned a compare-and-set that then threw.
- 30d0a2b: `files.capabilities` now reports `conditional.delete` as `false` when `files-sdk/soft-delete` is installed, matching the plugin's refusal of a conditional delete outside the trash. Previously it kept advertising the adapter's native support, so callers that branched on the snapshot planned a conditional delete that then threw.
- 30d0a2b: `files.capabilities` now reports every `conditional` flag as `false` when `files-sdk/tiering` is installed, matching the plugin's refusal of every conditional operation. Previously it kept advertising the hot adapter's native support, so callers that branched on the snapshot planned a compare-and-set that then threw.
- 30d0a2b: A cross-tier `copy()` or `move()` under `files-sdk/tiering` now applies the call's `signal` and `timeout` to the upload into the destination tier, and a `fallback: true` copy or move applies them to the eviction of the destination's stale copy. Previously only the read from the source tier honored them, so a hung destination tier ignored the caller's abort or deadline and could stall the call indefinitely.
- 30d0a2b: `files-sdk/validation` now marks every `ValidationError`, and its `signedUploadUrl()` refusal, as `permanent`. Previously they were ordinary `Provider` errors, so a `failover()` placed before the plugin treated a rejected write as an outage and re-sent it to its secondary, which runs without the plugin, storing an upload that broke the size, type, or key rule.
- 30d0a2b: `files.capabilities` now reports `conditional.create`, `conditional.replace`, `conditional.delete` and every `conditional.copy` flag as `false` when `files-sdk/versioning` is installed, matching the conditional mutations the plugin refuses. Previously it kept advertising the adapter's native support, so callers that branched on the snapshot planned a compare-and-set that then threw. Exact reads (`conditional.exactRead`) still follow the adapter.
- 4ce2f07: `files-sdk/bun-s3` now infers each `list()` item's `type` from its key, as `files-sdk/s3` and `files-sdk/s3-fetch` already do. An S3 list response carries no `Content-Type`, and this adapter labelled every item `application/octet-stream`, so a `.csv` or `.png` listed as a binary blob. Keys with an unknown extension still fall back to `application/octet-stream`.
- 4ce2f07: `files-sdk/s3` (and the S3-compatible adapters built on it) now reports a failed body read on a `head()` result or a `list()` item as a `FilesError`. Previously the lazy `GetObject` behind `text()`, `arrayBuffer()`, `blob()`, and `stream()` let the raw AWS SDK exception escape, so an object deleted after it was listed surfaced as an `S3ServiceException` instead of a `NotFound` error. The `fetch` engine (`files-sdk/s3-fetch`, `client: "fetch"`) already behaved this way.
- 4ce2f07: `files-sdk/s3` (and every adapter built on it: r2, minio, rustfs, wasabi, and the other S3-compatible wrappers) now detaches its abort listener once a multipart, progress, or unknown-length stream upload finishes. Previously the listener stayed on the caller's `signal` until that signal aborted, so a long-lived signal such as a `Files`-level default kept every finished upload, and the body it held, in memory.
- 4ce2f07: `files-sdk/s3` (and the S3-compatible adapters built on it) now marks the error for a missing `@aws-sdk/lib-storage` peer as permanent and keeps the import failure as its `cause`. Previously a `multipart`, `onProgress`, or unknown-length stream upload without the package installed was retried under `retries`, re-issuing an upload that could only fail the same way.
- 4ce2f07: `files-sdk/neon` now throws at construction when only one of `accessKeyId` and `secretAccessKey` is passed. Previously the key that was passed was silently dropped and the adapter fell back to the AWS credential chain, so a half-configured pair (for example, a secret read from an unset environment variable) connected with whatever credentials the environment held instead of failing.
