haydenbleasel/files-sdk
A unified storage SDK for object and blob backends. One small, honest API. Web-standards I/O.
12 Releases
Latest: yesterday
files-sdk@1.9.0Latest
๐ Minor Changes
- ff814cc: Add an `audit()` plugin at `files-sdk/audit` that writes a structured who/what/when record of every mutation to an awaited sink โ the durable, awaitable counterpart to the fire-and-forget `onAction` hook. Each audited operation produces one `AuditRecord` carrying the verb, the caller-facing key (or `from` / `to`), an optional `actor`, the start time and duration, the outcome, and โ on a successful `upload` โ the stored size. Because the sink is awaited, the operation doesn't resolve until the record is written, giving you ordering and back-pressure a hook can't: on a successful operation a rejecting sink fails the call (the mutation happened but wasn't recorded โ fail closed), while on a failed operation the operation's own error always wins so a sink problem can never mask why the call failed. By default it records the mutating verbs (`upload`, `delete`, `copy`, `move`, `signedUploadUrl`); pass `events: "all"` to also audit reads, or an explicit list to record exactly the verbs you name. Resolve `actor` synchronously from your request context to attribute each record. It's body-transparent (never buffers, transforms, or reads the body โ `size` comes from declared metadata), writes no object metadata, and has no native dependencies, so it works on any adapter. Plugins run outside retries (so a retried call is still one record) on caller-facing keys; bulk `upload([...])` / `delete([...])` fan out to one record per item, each flagged `bulk: true`. It's `wrap`-only, so plain `new Files({ plugins })` works. Place it first (outermost) so it records the caller's logical intent โ a `delete` an inner `softDelete()` turns into a `move` is still audited as the `delete` the caller asked for.
- daca585: Add a `cache()` plugin at `files-sdk/cache` โ an LRU/KV cache in front of the cheap read verbs. A repeat `head()` or `url()` (and, opt-in, a small `download()`) for an unchanged key is served from memory instead of round-tripping to the provider; any write through the instance (`upload`, `delete`, `copy`, `move`) invalidates the affected key so the next read re-fetches. `head` caches metadata only (a hit's body still lazy-fetches on access, matching the uncached `head` contract); `url` caches per url-options signature and caps each entry at its own `expiresIn` so a presigned URL is never handed out past its signature; `download` is off by default and, when enabled via `operations: ["download"]`, buffers only known-length bodies at or under `maxBytes` (default 1 MiB) so streaming and large objects keep working. Defaults to a bounded in-memory LRU (`maxEntries`, default 1000), or pass your own `CacheStore` to back it with a shared KV. Entries honor a `ttl` (default 60s; `0` disables time-based expiry). It writes no object metadata and has no native dependencies, so it works on any adapter, and runs outside retries so a hit skips the retry loop entirely. It uses `extend` for `invalidateCache(key?)`, `cacheStats()`, and `resetCacheStats()` โ construct with `createFiles` to surface them on the type. Place it first (outermost) so a hit short-circuits before the rest of the pipeline does any work; writes made out-of-band (a presigned-URL upload, or a change straight against the provider) won't invalidate, so call `invalidateCache()` and treat the cache as eventually-consistent.
- 83d6eb4: Add a `failover()` plugin at `files-sdk/failover` that reads/writes the primary and falls back to one or more secondary adapters when a backend is down โ a live, per-operation failover chain. The primary is the instance's own adapter (reached through the rest of the onion, so it keeps retry and prefixing); the secondaries are backup adapters passed in `secondaries` (a single `Adapter` or an array for a multi-region chain), each wrapped in its own internal `Files` so it gets the same retry, capability gating, and `StoredFile` normalization. Every verb runs the same way: try the primary; if it throws and `shouldFailover` says so, try the next backend, and so on โ the first to succeed wins, and if the chain is exhausted the last error is thrown. The default predicate fails over only on `Provider` errors (network / timeout / 5xx โ "the backend is down") and never on an aborted request or a definitive answer from a healthy backend (`NotFound`, `Unauthorized`, โฆ), so a genuine 404 stays a 404 instead of being masked by a replica; pass your own `shouldFailover` to widen it (e.g. read through to a replica on `NotFound`) or narrow it. This is the availability counterpart to `tiering()` (which _partitions_ by key/size): failover treats each secondary as a full replica, so it never splits or merges across backends โ `list` returns the first reachable backend's page (not a merged one), and writes land on the first reachable backend rather than fanning out to all (that's `replication()`). A streaming `upload` (a `ReadableStream` body) can't be replayed, so it runs against the primary alone and isn't failed over. An optional `onFailover` callback (fire-and-forget; a throw from it is swallowed) reports each fail over with the operation and the backend indices, for metrics / alerting. It's body-transparent, has no native dependencies, and adds no surface (`wrap` only), so it works with plain `new Files({ plugins })`. Place it last (innermost) so body-transforming plugins like `encryption()` wrap every backend, and give each secondary its own bucket / container (secondaries receive caller-facing keys, without the instance `prefix`). Failover buys availability, not convergence โ reconcile a secondary written during an outage with `sync` / `transfer`, or keep it current with `replication()`.
- 581c97f: Add a queryable `files.capabilities` surface that reports what the underlying adapter can do, so callers, AI tool wrappers, and validators can branch up front instead of relying on a throw at call time. It returns an `AdapterCapabilities` snapshot with eight fields, each mirroring an operation the unified API actually exposes: `rangeRead`, `uploadProgress`, `delimiter`, `metadata`, `cacheControl`, and `multipart` are derived live from the same per-adapter flags and optional methods the wrapper already gates on (so they can never drift from runtime behavior), while `serverSideCopy` and `signedUrl` (`{ supported; maxExpiresIn? }`) are declared per-adapter and default to the conservative value when unset โ a caller that doesn't advertise reads as "no", never a wrong "yes". `signedUrl.supported` is `true` when `url()` can mint a signed or tokenized URL (not just a permanent public link); `maxExpiresIn` is set only where a provider enforces a hard `expiresIn` ceiling in code (e.g. Dropbox's 4-hour temporary links), not for soft infra limits or config-dependent caps. Custom adapters can set the new optional `supportsServerSideCopy` and `signedUrl` fields alongside the existing `supports*` flags; both are advisory and gate nothing. See the new Capabilities and Provider gaps documentation.
- 81e0e64: Add a `neon` adapter at `files-sdk/neon` for [Neon](https://neon.com) branchable object storage over its S3-compatible API. A thin wrapper around the S3 adapter โ errors relabelled, with path-style addressing on by default because Neon requires it (the wildcard TLS cert covers a single subdomain level, occupied by the branch id, so the bucket name travels in the request path). It reads the standard `AWS_*` variables that `neon dev` / `neon env pull` inject for the linked branch โ `endpoint` from `AWS_ENDPOINT_URL_S3`, region from `AWS_REGION` (then `NEON_STORAGE_REGION`, then `us-east-1`), and credentials through the AWS SDK chain (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`) โ so inside a Neon Function or after an env pull it works from env alone: `neon({ bucket: "images" })`. Catalogued in `files-sdk/providers` and exposed through the CLI.
- 2275982: Add an opt-in `receipts` option that surfaces a provenance `Receipt` for each mutating call (`upload`, `delete`, `copy`, `move`) โ built for AI tool wrappers and agents that need to attest "this exact content landed at this key". It's off by default: an instance without the option records nothing and hashes nothing, so existing behavior is unchanged. Turn it on with `receipts: true` to attach a `Receipt` (`{ op, provider, key, bytes?, etag?, sha256?, durationMs, ts }`) to the success `onAction` event of each mutating call โ an additive `receipt` field on the existing hook, with no new operation, callback, or changed return type. Every field except `sha256` is derived from the work the SDK already does for the hook (timing, the adapter name, the caller-facing key, and `bytes` / `etag` read straight off the `UploadResult`), so plain `receipts: true` adds no per-call cost. `sha256` is the one field with a real per-call cost and is opt-in by name: pass `receipts: { sha256: true }` to fingerprint the upload body as passed to `upload()` โ taken before any plugin transform, so it matches what `download` gives back rather than the (possibly encrypted/compressed) bytes on disk โ a lowercase-hex SHA-256, present only on an `upload` of a buffered body. A streaming upload is never buffered to hash it (so it carries no fingerprint), and `delete` / `copy` / `move` transfer no content of their own; with `sha256` off, the body is never read. Reads, `signedUploadUrl`, failures, bulk array calls, and receipts-off instances all leave `event.receipt` unset. See the new Receipts documentation.
- 5a77a58: Add a `signedUrlPolicy()` plugin at `files-sdk/signed-url-policy` that enforces safe defaults on the two URL-minting operations, turning the security caveats `url()` and `signedUploadUrl()` document into the default. On `url()` it forces a download `Content-Disposition` (default `"attachment"`, so user-uploaded HTML/SVG can't execute inline at your origin โ the stored-XSS warning made a default) while preserving a caller's existing `attachment` (and its `filename`), and clamps `expiresIn` to `maxExpiresIn`. On `signedUploadUrl()` it clamps `expiresIn` to the same cap and, when `maxUploadSize` is set, guarantees a server-enforced `maxSize` is always present (injected when absent, clamped when over) โ so an adapter that can't bind a size limit fails closed loudly instead of minting an unbounded URL. It writes no metadata, transforms nothing on disk, never throws of its own accord, and lets every other verb pass straight through; with no options set it still applies the headline default (`url()` forces `attachment`). Set `disposition: false` to opt out of the disposition guard. Place it first (outermost) so it sees the caller's original request and its rewritten options reach the signing adapter.
- ae58680: Add a `softDelete()` plugin at `files-sdk/soft-delete` that turns `delete` into a recoverable move into a trash prefix - a recycle bin for any adapter. Instead of destroying an object, a `delete` server-side moves it to `"<prefix>/<key>"` (`.trash/` by default); the bytes only leave storage when you `purge()`. It adds three methods via `extend` (so construct with `createFiles`): `trashed()` lists what's in the trash (each entry carries the original `key` plus a downloadable `trashKey`), `restore(key)` moves the trashed copy back over the live key (overwriting a re-created one, throwing when nothing's trashed), and `purge(key?)` permanently deletes one item or empties the whole trash (idempotent). Like `versioning()` it's body-transparent - it never buffers, transforms, or reads the body, so streaming, range downloads, `url()`, and `signedUploadUrl()` all keep working - and has no native dependencies. Trashed objects are hidden from `list()` (unless you list within the prefix); a `delete` of a key inside the trash prefix is a real delete (that's how `purge()` works); deleting a missing key stays a no-op; and bulk `delete([...])` soft-deletes every key. One trashed copy is kept per key (re-deleting replaces it - reach for `versioning()` to keep every generation). Place it first (outermost) so it relocates whatever the rest of the pipeline stored.
- + 3 more
๐ Patch Changes
- 53da200: Document why the Azure resumable-upload probe is safe to treat staged (uncommitted) blocks as skippable: blocks Azure garbage-collects before finalization make `commitBlockList` fail loudly (`InvalidBlockList`) rather than committing with gaps, and a retry re-probes correctly. Comment-only; no behavior change.
- bcad8b4: Fix untyped `Blob`/`File` uploads being sent with an empty `Content-Type`. `Blob.type` is `""` (never nullish) when no type was given, so the documented `application/octet-stream` fallback behind a `??` was dead code โ the provider received `contentType: ""`. Fixed in the core body normalizer and the same pattern in the box, onedrive, supabase, google-drive, dropbox, r2, uploadthing, and convex adapters.
- 77f6bc6: Fix the array forms of `download`/`head`/`exists`/`delete` ignoring the constructor-level `signal` and `timeout` defaults. The bulk bases call the adapter directly to stay retry-free (as documented), but that also skipped the instance-wide abort signal and timeout โ aborting the constructor signal mid-bulk cancelled nothing, and a configured `timeout` never bounded bulk reads or deletes (bulk upload already honored both). Bulk per-item calls now run under the same signal/timeout plumbing as single operations, still without retries.
- 15567cf: Fix the bulk worker pool dying on a sparse/`undefined` array slot. The per-worker guard `return`ed instead of skipping the slot, so with `concurrency: 1` (or as many holes as workers) every key after the hole was silently neither processed nor reported in `results`/`errors`. Only reachable past the type system (a sparse array or an `undefined` element cast in), but the recovery is now to skip just that slot.
- 56965b4: Fix `cache()` serving presigned URLs past their signature when `url()` is called without `expiresIn`. The signature-lifetime cap only applied when the caller passed `expiresIn`, but the adapter signs default calls with a finite lifetime too โ so with a long `ttl` (or `ttl: 0`, which disables time-based expiry entirely) the cache kept handing out dead links indefinitely. Entries for default-signed URLs are now capped at the assumed signature lifetime, configurable via the new `defaultUrlExpiresIn` cache option (defaults to the SDK-wide 3600s; set it to match your adapter if you changed its default).
- 0d45bb7: Fix CLI and MCP output of bulk partial-failure errors. The `errors` arrays embed live `FilesError` instances, and a bare `JSON.stringify` drops `message` (a non-enumerable `Error` property) while serializing the enumerable `cause` โ the raw provider error, which can carry request ids and response headers the SDK explicitly warns against shipping across a trust boundary. All CLI/MCP serialization now goes through a replacer that emits `{ code, message, aborted, timedOut }` for any embedded `FilesError` and strips `cause`.
- 30f75cc: Fix the CLI truncating piped output on partial failures. Commands called `process.exit()` immediately after writing the structured result to stdout, and POSIX pipe writes are asynchronous โ a large payload (e.g. a bulk `head` with errors) could be cut off mid-JSON before the consumer received it. Commands now signal failure via `process.exitCode` and let the process end once stdout drains.
- 74a1226: Fix the CLI eagerly importing optional provider peer dependencies. The bundler previously inlined the registry's lazily-imported provider modules into `dist/cli/index.js`, hoisting their external imports (e.g. `@netlify/blobs`) to the top level โ so `files --help` crashed with `ERR_MODULE_NOT_FOUND` unless every optional peer was installed. The build now emits shared chunks so the registry's `await import(...)` calls stay genuinely dynamic: provider-independent commands run without any optional peers installed, and a missing peer only surfaces when its provider is actually selected.
- + 26 more
files-sdk@1.8.0
๐ Minor Changes
- 87607ec: Add a `compression()` plugin at `files-sdk/compression` for transparent, at-rest compression. Bodies are gzipped (or deflate / deflate-raw) on upload with the algorithm and original size recorded in metadata, and decompressed on download (bulk calls too); incompressible data is stored verbatim so storage never grows. Uses only the Compression Streams API โ no native dependencies โ and works on any adapter that supports metadata.
- d2fa5e0: Add a `contentType()` plugin at `files-sdk/content-type` that decides an upload's `Content-Type` from its bytes instead of the client's claim. It magic-byte-sniffs the body on `upload` and either corrects the stored type to match (the default) or rejects a mismatch, so a `.png` whose bytes are really HTML/SVG can't be stored under an image type and served inline. Recognizes the common images, PDF, and โ via a leading text scan โ HTML, SVG, and XML. It writes no metadata and only reads the first 512 bytes, so known-length bodies are peeked with no copy and streams stay streaming; `signedUploadUrl()` fails closed (a direct client upload bypasses the sniff). Also exports `detectContentType()`. No native dependencies; works on any adapter.
- 5ad680e: Add a `dedup()` plugin at `files-sdk/dedup` for content-addressed de-duplication. On `upload` the body is hashed (SHA-256) and its bytes are stored only once at a content-addressed blob under a store prefix (`.dedup/` by default); the logical key holds a tiny pointer to it, so re-uploading content already in the store skips the byte upload, and `copy` / `move` of a de-duplicated file is near-free and shares the blob. Reads are transparent โ `download` follows the pointer (ranges included, since blobs are stored verbatim), and `head` / `list` report the logical size with internal fields stripped โ for bulk calls too. Uses only the Web Crypto API โ no native dependencies โ and works on any adapter that supports metadata. It buffers the body to hash it (so it doesn't suit unknown-length streams or resumable uploads), `url()` / `signedUploadUrl()` fail closed, and orphaned blobs aren't garbage-collected. Place it before `compression()` / `encryption()` in the array โ encrypted bytes don't de-dup.
- feaf806: Add an `encryption()` plugin at `files-sdk/encryption` for provider-agnostic, at-rest envelope encryption. A per-object data key encrypts the body with AES-256-GCM and your master key wraps it into the object's metadata; downloads decrypt transparently (bulk calls too). Uses only the Web Crypto API โ no native dependencies โ and works on any adapter that supports metadata. Also exports `generateEncryptionKey()`.
- 4d40229: Add a `files.search(pattern, options?)` method that finds objects whose key matches a pattern. By default `pattern` is a standard glob (powered by picomatch: `*` within a path segment, `**` globstar across segments, `?`, `[a-z]` classes, `{a,b}` braces, `!` negation; a glob with no wildcards is an exact match); set `match` to `"regex"`, `"substring"`, or `"exact"`, or pass a `RegExp` directly, to change that. It returns a streaming async iterable of `StoredFile` built on `listAll`, so it walks every page lazily (stays memory-bounded, `break` or `maxResults` to stop early) and works on every adapter with no per-provider capability. A glob's literal prefix is pushed down to the underlying `list` automatically (`uploads/2024/*.pdf` scopes the walk to the `uploads/2024` prefix); for a regex/substring/case-insensitive search, pass `prefix` to bound the walk. The CLI gains a `files search <pattern>` command (`--match`/`--regex`/`--prefix`/`--limit`/`--max-results`/`--case-insensitive`) and the MCP server a `search` tool.
- 3a42a18: Add an opt-in plugin system to `Files`. Plugins wrap every operation in an ordered onion โ they can transform, veto, or observe (the interceptable superset of `hooks`) โ and can contribute new namespaced surface.
- ```ts
- const files = createFiles({
- + 16 more
๐ Patch Changes
- 5ad680e: Fix plugin cross-kind re-routing inside bulk operations. A plugin whose `wrap` calls `next()` with a different verb than the one it's intercepting โ e.g. `dedup()`'s `exists` probe, or `versioning()`'s snapshot `head` + `copy` โ misrouted when it ran inside `upload([...])` / `download([...])` / `head([...])` / `exists([...])` / `delete([...])`, because each bulk item was dispatched with a base locked to that one verb. The bulk bases now delegate any re-routed, cross-kind sub-op to the single-operation path, so it behaves identically in a bulk call as in a single one; the item's own verb keeps its retry-free, hook-quiet semantics.
- 0f3771e: Switch the build from tsup to Bun's bundler (for JavaScript) plus tsgo (for type declarations), orchestrated by `scripts/build.ts`. tsup is no longer maintained and its declaration emit needed an enlarged Node heap; the replacement builds the whole package โ every adapter, plugin, and the CLI โ in well under a second with no heap flag. The published ESM output and `exports` map are unchanged, so imports resolve identically. The only packaging difference is that type declarations are now emitted per source file rather than rolled up into bundled `.d.ts` files; type resolution for consumers is equivalent.
files-sdk@1.7.0
๐ Minor Changes
- 3c8abf3: Add `sync()` โ an incremental, optionally-pruning mirror between two providers. It skips objects already identical at the destination (compare by size + etag, size, or a custom predicate), can prune destination keys the source no longer has (mirror mode), and supports `dryRun` to preview the reconciliation plan. Surfaced at parity as the CLI `sync` command and a write-gated MCP `sync` tool.
- d998ef6: Add directory-style listing to `list`: a new `delimiter` option collapses keys into S3-style common prefixes ("folders"), returned in `ListResult.prefixes`. Supported on every adapter with a folder or prefix model โ the object stores (S3 family, R2, GCS, Firebase Storage, Azure) and `fs`/memory/FTP/SFTP/Google Drive/Cloudinary accept any delimiter; the folder-based providers (Vercel Blob, Netlify Blobs, Supabase, Dropbox, Box, OneDrive, SharePoint) accept `"/"`. Adapters with no folder concept (UploadThing, Appwrite, PocketBase, Convex, Bun's S3) advertise `supportsDelimiter: false` and throw rather than silently returning a flat list.
- 0345169: Add read-only `Files` instances.
- Pass `readonly: true` to the constructor, or derive a locked view from an existing client with `files.readonly()`, when a caller should be able to read storage but never mutate it:
- ```ts
- const files = new Files({
- adapter: s3({ bucket: "uploads" }),
- readonly: true,
- + 21 more
๐ Patch Changes
- 1ff2550: Azure gains a native `deleteMany` backed by the Blob Batch API (256 keys per batch, idempotent on already-missing blobs); `stopOnError` falls back to sequential deletes. Previously it fanned out to single deletes.
- e1d09a6: Validate Microsoft Graph pagination cursors against the adapter root before following them for OneDrive and SharePoint list calls.
- e1d09a6: Cap AI tool download `maxBytes` overrides at 10 MiB and reject oversized values in both schema validation and direct executor calls.
- e1d09a6: Bound CLI MCP downloads by checking object metadata and requested byte ranges before transferring response bodies.
- e1d09a6: Reject `.` and `..` segments in `Files` prefixes and prefixed keys before resolving local filesystem paths, so prefixed fs adapters cannot escape their configured root.
- 1ff2550: FTP & SFTP `move()` now uses a native rename (`RNFR`/`RNTO` and the SFTP `RENAME` op) instead of a copy + delete body round-trip. The destination's parent directory is created first where needed.
- 1ff2550: FTP & SFTP now support ranged downloads (`download(key, { range })`): SFTP uses native read-stream `start`/`end` offsets; FTP begins the transfer at the `REST` start offset and trims a bounded `end` client-side. Both adapters now advertise `supportsRange`.
- e1d09a6: Start the MCP server in read-only mode by default and require `--allow-writes` before registering mutation tools.
- + 7 more
files-sdk@1.6.0
๐ Minor Changes
- 12d6218: Bring the CLI (and MCP server) to full parity with the SDK surface.
- Every `Files` capability is now reachable from the `files` binary:
- Global `--key-prefix` scopes every operation under a base path (the instance prefix from `new Files({ prefix })`, distinct from the one-off `list --prefix` filter). Global `--timeout` / `--retries` set the per-attempt timeout and retry count for all commands.
- `download --range start-end` downloads a byte range (0-based, inclusive), e.g. `0-1023` or `1024-`.
- `upload --multipart` (with `--part-size` / `--multipart-concurrency`) uploads large objects in parallel parts.
- `head` / `exists` / `delete` accept `--concurrency` and `--stop-on-error` to tune the bulk fan-out for many keys.
- `list --all` walks every page (following the cursor) and returns all items in one result.
- `upload --dir <localDir>` uploads a whole local tree (keyed by relative path, content type inferred per file), and `download <keys...> --out-dir <dir>` downloads many keys into a directory โ both built on the SDK's bulk array forms.
- + 82 more
๐ Patch Changes
- 52daa66: Update bundled and peer dependencies.
- The CLI's `commander` runtime dependency moves to v14. Several optional provider-SDK peer floors are raised to the majors now built and tested against:
- `@anthropic-ai/claude-agent-sdk` โ `^0.3.0` (claude adapter)
- `@googleapis/drive` โ `^20.0.0` (google-drive adapter)
- `google-auth-library` โ `^10.0.0` (gcs / google-drive auth)
- `node-appwrite` โ `^25.0.0` (appwrite adapter)
- `pocketbase` โ `^0.27.0` (pocketbase adapter)
- No public API or behaviour changes. If you use one of the adapters above, upgrade its peer to the new major.
- + 4 more
files-sdk@1.5.0
๐ Minor Changes
- c6b4df1: `upload`, `download`, `head`, and `exists` now accept an array for bulk operations, mirroring `delete`. Pass the usual single argument for the original behavior (resolves to one result, throws on failure); pass an array to operate on many in one call and get back a structured result instead of throwing on partial failure โ so you can see exactly which keys succeeded and which failed:
- ```ts
- const up = await files.upload(
- [
- { key: "avatars/a.png", body: a, contentType: "image/png" },
- { key: "avatars/b.png", body: b },
- ],
- { concurrency: 8, stopOnError: false }
- + 32 more
๐ Patch Changes
- e80e922: Add `signal`, `timeout`, and `retries` to every operation. Set them on the `Files` constructor as defaults and override per call (a per-call value wins). `retries` is a number or `{ max, backoff }`; only `Provider` failures are retried โ `NotFound`, `Unauthorized`, `Conflict`, aborts, and timeouts are returned immediately, and `ReadableStream` uploads are never retried because a consumed stream can't be replayed. The default backoff is exponential (`100 * 2 ** (attempt - 1)` ms, capped at 30s, no jitter); pass your own `backoff({ attempt, error })` for jitter or a different curve. `timeout` is applied per attempt and aborts the operation rather than triggering a retry. A `signal` always fails fast at the `Files` layer for every adapter; the underlying provider request is also cancelled on the S3 adapter and the S3-compatible catalog, Vercel Blob and UploadThing's fetch-backed reads, Azure, Google Drive, and PocketBase (across their operations), Supabase (`download` and `list` โ the only methods its SDK lets a signal through), and the fetch-backed downloads of Box, Cloudinary, and Dropbox. Adapters whose SDK exposes no cancellation (GCS, Firebase Storage, Netlify Blobs, Appwrite, Bunny, Bun S3, and the R2 binding path) still fail fast at the `Files` layer but leave the in-flight request running.
- f774aa2: Add Azure AD / Managed Identity support to the Azure adapter via a `credential` (`TokenCredential`) option. Token-authenticated adapters mint User Delegation SAS URLs for `url()`, `signedUploadUrl()`, and same-container `copy()`, so signed URLs keep working without a storage account key. Set `useUserDelegationSas: false` to opt out of SAS signing for token-only setups.
- dbda237: Add a `prefix` option to the `Files` constructor. When set, every key is resolved relative to the prefix - reads, writes, copies, listings, URLs, and signed uploads - and the prefix is stripped back off the keys (and `name`) returned in results, so your application code works in its own namespace:
- ```ts
- const users = new Files({
- adapter: s3({ bucket: "uploads" }),
- prefix: "users",
- });
- + 7 more
files-sdk@1.4.0
๐ Minor Changes
- ef0d6af: Add Alibaba Cloud Object Storage Service (OSS) adapter (`files-sdk/alibaba`). Thin wrapper around the S3 adapter โ endpoint derived from the region code (`oss-<region>.aliyuncs.com`), virtual-hosted-style addressing, errors relabelled as "Alibaba Cloud error". Auto-loads from `ALIBABA_ACCESS_KEY_ID` and `ALIBABA_ACCESS_KEY_SECRET`.
- d619709: Add `files` CLI for agents and scripts. One binary covers every adapter via `--provider <name>` with lazy imports โ cold-start cost matches whichever single provider you select. Each `Adapter` method maps to a subcommand (`upload`, `download`, `head`, `exists`, `delete`, `copy`, `list`, `url`, `sign-upload`), with JSON-by-default output, `stdin`/`stdout` streaming for binary bodies, `--dry-run` and `--verbose` modes, and a stable exit-code mapping (`NotFound` โ 1, `Provider` โ 2, `Unauthorized` โ 3, `Conflict` โ 4). Provider credentials come from each adapter's existing env-var conventions, and `--config-json` is an escape hatch for the long tail of adapter options. `files ... mcp` boots a stdio MCP server exposing every command as a tool โ provider and credentials bind at startup, so the agent only passes operation arguments.
- d0aec82: Add Cloudinary adapter (`files-sdk/cloudinary`). Defaults to `resource_type: "raw"` for arbitrary-bytes storage; switch to `image`/`video` for transforms. Reads `CLOUDINARY_URL` or individual `CLOUDINARY_*` env vars. Full Adapter surface including signed delivery URLs for `private`/`authenticated` types and form-POST signed upload URLs.
- 8b62142: Add Firebase Storage adapter (`files-sdk/firebase-storage`). Wraps the official `firebase-admin` SDK; the underlying `getStorage().bucket()` returns a `@google-cloud/storage` `Bucket`, so V4 signed read URLs, POST policy uploads with `maxSize`, server-side copy, and the full metadata round-trip all work out of the box. Auto-loads credentials from `FIREBASE_PROJECT_ID` / `FIREBASE_CLIENT_EMAIL` / `FIREBASE_PRIVATE_KEY` / `FIREBASE_STORAGE_BUCKET`, falling back to a service-account JSON path (`GOOGLE_APPLICATION_CREDENTIALS`) and then to Application Default Credentials. Accepts an existing `App` or `Bucket` via `app` to share initialization with Firestore/Auth. The bucket name defaults to `<projectId>.firebasestorage.app` when neither `bucket` nor `FIREBASE_STORAGE_BUCKET` is set. Firebase's `?alt=media&token=โฆ` download-token URL form is out of scope for v1 โ reach for `adapter.raw` if you need it.
- 8b62142: Add PocketBase adapter (`files-sdk/pocketbase`). Wraps the official `pocketbase` JS SDK and maps the unified key/blob API onto a dedicated collection: each upload becomes (or updates) a record whose configurable `keyField` (unique-indexed text, default `"key"`) holds the user-facing key and whose configurable `fileField` (single-value file, default `"file"`) holds the body. Auto-loads from `POCKETBASE_URL` plus either `POCKETBASE_ADMIN_EMAIL` + `POCKETBASE_ADMIN_PASSWORD` (admin login on first call) or `POCKETBASE_AUTH_TOKEN` (pre-issued token); accepts an existing `PocketBase` client via `client`. `url()` returns `pb.files.getURL()`, threading a short-lived file token from `pb.files.getToken()` for authenticated clients; set `publicBaseUrl` for a CDN override. `signedUploadUrl()` throws โ PocketBase has no presigned upload primitive. `copy()` is read-then-write (no server-side copy). `list()` paginates via page number encoded as a numeric cursor string. `UploadOptions` `cacheControl` and `metadata` throw โ PocketBase has no per-file HTTP cache headers and no arbitrary-metadata field on the file; add extra typed columns to the collection and write via `raw` if you need them. `responseContentDisposition` on `url()` throws โ use `raw` and the `?download=true` query string instead.
- d0aec82: Add SharePoint adapter (`files-sdk/sharepoint`). Resolves `siteUrl` and named `documentLibrary` to a drive via Microsoft Graph, then delegates to the OneDrive adapter for file operations. Falls back to `SHAREPOINT_*` env vars then to `ONEDRIVE_*`. Resolution is lazy and cached after the first call.
- ef0d6af: Add Tencent Cloud Object Storage (COS) adapter (`files-sdk/tencent`). Thin wrapper around the S3 adapter โ endpoint derived from the region code (`cos.<region>.myqcloud.com`), virtual-hosted-style addressing, errors relabelled as "Tencent Cloud error". Auto-loads from `TENCENT_SECRET_ID` and `TENCENT_SECRET_KEY`. Bucket name must include the `-<appid>` suffix per COS's namespacing.
- ef0d6af: Add Yandex Object Storage adapter (`files-sdk/yandex`). Thin wrapper around the S3 adapter โ fixed global endpoint (`storage.yandexcloud.net`), region defaults to `ru-central1` for signing, virtual-hosted-style addressing, errors relabelled as "Yandex Cloud error". Auto-loads from `YANDEX_ACCESS_KEY_ID` and `YANDEX_SECRET_ACCESS_KEY`.
- + 8 more
๐ Patch Changes
- a53be2d: Expand adapter test coverage for error-recovery branches that were previously unexercised: `exists()` swallowing a thrown `NotFound` (azure, gcs, netlify-blobs, r2) versus rethrowing other mapped errors; the supabase stream-download error envelope; and dropbox's `exists()` returning false for `folder`/`deleted` `.tag`s plus the `shared_link_already_exists` recovery falling through when no usable URL is embedded. No runtime behavior changes.
files-sdk@1.3.0
๐ Minor Changes
- 2d3a569: Add Appwrite adapter at `files-sdk/appwrite` exporting `appwrite()`, a wrapper around the official `node-appwrite` SDK's `Storage` API. Auto-loads `endpoint`, `projectId`, and `key` from `APPWRITE_ENDPOINT` / `APPWRITE_PROJECT_ID` / `APPWRITE_API_KEY` (with `NEXT_PUBLIC_*` fallbacks for the first two), or accepts an existing `Client` or `Storage` instance via `client`. `list({ prefix })` is forwarded as a `startsWith("$id", prefix)` query against the canonical file ID โ files created outside the adapter where the display `name` differs from `$id` won't be matched by prefix. `upload()` buffers stream bodies up-front since `InputFile.fromBuffer` has no streaming form, throws on `UploadOptions.cacheControl` and non-empty `UploadOptions.metadata` (Appwrite has no equivalent fields), and silently ignores `UploadOptions.contentType` (Appwrite auto-detects mime from the payload). `copy()` is read-then-write โ Appwrite has no server-side copy primitive, so it costs an egress + an ingest and is not atomic. `url()` throws by default (Appwrite SDKs cannot mint signed read URLs with API keys); set `public: true` on a public bucket to return the constructed permanent `view` URL. `signedUploadUrl()` throws โ Appwrite has no presigned upload primitive; use JWTs or the client SDK for direct uploads. Keys (Appwrite file IDs) must start with `[a-zA-Z0-9]` and use only `[a-zA-Z0-9._-]`, max 36 characters โ invalid keys throw a `FilesError("Provider", ...)` before the API call. Errors are relabelled as `Appwrite error`, with `404`/`401`+`403`/`409` mapped to `NotFound`/`Unauthorized`/`Conflict`.
- ed87e51: Add Backblaze B2 adapter at `files-sdk/backblaze-b2`, a thin S3 wrapper that derives the endpoint from the cluster code (`s3.<region>.backblazeb2.com`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `B2_APPLICATION_KEY_ID` / `B2_APPLICATION_KEY`. Errors are relabelled as `Backblaze B2 error` and `publicBaseUrl` accepts B2's friendly download URL prefix for skipping signing on public buckets.
- 2a35ce1: Add `exists(key)` to the Files API. Returns `true` when the object exists and `false` when the adapter reports a not-found error, without fetching the object body. Implemented across all built-in adapters.
- 8ae51f0: Add Exoscale Object Storage (SOS) adapter at `files-sdk/exoscale`, a thin S3 wrapper that derives the endpoint from the zone code (`sos-<region>.exo.io` โ `ch-gva-2`, `ch-dk-2`, `de-fra-1`, `de-muc-1`, `at-vie-1`, `at-vie-2`, `bg-sof-1`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `EXOSCALE_API_KEY` / `EXOSCALE_API_SECRET`. Exoscale calls these zones but they fill the SigV4 region slot. Errors are relabelled as `Exoscale error`.
- 2c52f56: Add `files.file(key)` to return a `FileHandle` bound to a single key. The handle exposes `upload`, `download`, `head`, `exists`, `delete`, `url`, `signedUploadUrl`, `copyTo`, and `copyFrom` without re-passing the key each time. It's a thin wrapper over the same `Files` methods, so adapters do not need to implement anything extra.
- 8ae51f0: Add Filebase adapter at `files-sdk/filebase`, a thin S3 wrapper around Filebase's S3-compatible gateway in front of decentralized storage networks (IPFS, Sia, Storj โ the backing network is chosen per-bucket in the dashboard). Uses the fixed `https://s3.filebase.com` endpoint with virtual-hosted-style addressing, defaults the SigV4 region to `"us-east-1"`, and auto-loads credentials from `FILEBASE_ACCESS_KEY_ID` / `FILEBASE_SECRET_ACCESS_KEY`. `publicBaseUrl` accepts an IPFS/Sia/Storj gateway prefix for skipping signing on public objects. Errors are relabelled as `Filebase error`.
- 8ae51f0: Add IBM Cloud Object Storage adapter at `files-sdk/ibm-cos`, a thin S3 wrapper that derives the endpoint from the region code (`s3.<region>.cloud-object-storage.appdomain.cloud` โ `us-south`, `us-east`, `eu-de`, `eu-gb`, `jp-tok`, `au-syd`, `br-sao`, `ca-tor`, โฆ), defaults to virtual-hosted-style addressing, and auto-loads credentials from `IBM_COS_ACCESS_KEY_ID` / `IBM_COS_SECRET_ACCESS_KEY`. Auth uses IBM Cloud's HMAC credentials (tick "Include HMAC Credential" in the service-credential Advanced options), not IAM API keys. For direct (no-egress) access from inside the same IBM Cloud region, pass `https://s3.direct.<region>.cloud-object-storage.appdomain.cloud` as an explicit `endpoint`. Errors are relabelled as `IBM Cloud Object Storage error`.
- 8ae51f0: Add iDrive e2 adapter at `files-sdk/idrive-e2`, a thin S3 wrapper that takes an explicit `endpoint` (iDrive e2 hostnames are tied to the provisioned bucket cluster and don't follow a public pattern โ copy it from the iDrive e2 dashboard under Access Keys โ Endpoint), defaults the SigV4 region to `"us-east-1"`, and auto-loads credentials from `IDRIVE_E2_ACCESS_KEY_ID` / `IDRIVE_E2_SECRET_ACCESS_KEY`. Errors are relabelled as `iDrive e2 error`.
- + 6 more
๐ Patch Changes
- 2aa92e1: URL-encode keys in `joinPublicUrl` to prevent injection attacks via special characters (`?`, `#`, spaces) in file keys. Uses segment-by-segment encoding to preserve `/` as a path separator.
- Note: Pass raw keys โ this function handles encoding. Pre-encoded keys will be double-encoded (e.g. `%20` becomes `%2520`).
- 8982c51: Expand test coverage for `box`, `fs`, `onedrive`, `supabase`, and `openai/responses` adapters. Adds tests covering `mapBoxError` / `mapGraphError` non-API error shapes, trailing-slash key handling, no-extension content-type inference, cache-miss reuse and non-file conflict paths in Box, trailing-slash URL trimming in Supabase, and ENOENT mid-page plus non-ENOENT walk errors in the fs adapter. No behavior changes.
files-sdk@1.2.0
๐ Minor Changes
- 9758347: Add AI SDK tools subpath (`files-sdk/ai-sdk`) exporting `createFileTools(...)` โ wraps a configured `Files` instance as a set of Vercel AI SDK tools (`listFiles`, `getFileMetadata`, `downloadFile`, `getFileUrl`, `uploadFile`, `deleteFile`, `copyFile`, `signUploadUrl`) ready to plug into `generateText` / `streamText` / any agent. Mirrors `@github-tools/sdk`'s ergonomics: write tools require approval by default (configurable globally or per-tool via `requireApproval`), `readOnly: true` strips writes entirely, and `overrides` lets callers patch tool descriptions/titles/etc. without touching `execute`. Individual tool factories (`uploadFile`, `downloadFile`, โฆ) are also exported for cherry-picking. `ai` and `zod` are optional peer dependencies โ only required when consuming the new subpath.
- 2d811b1: Add Claude Agent SDK tools subpath (`files-sdk/claude`) exporting `createClaudeFileTools(...)` โ wraps a configured `Files` instance as an in-process MCP server ready to drop into `query()` from [`@anthropic-ai/claude-agent-sdk`](https://docs.claude.com/en/api/agent-sdk/overview) (the renamed Claude Code SDK).
- ```ts
- const tools = createClaudeFileTools({ files });
- for await (const msg of query({
- prompt: "List my files.",
- options: {
- mcpServers: tools.mcpServers,
- + 11 more
files-sdk@1.1.2
๐ Patch Changes
- 6edb433: `googleDrive` and `onedrive` adapters now auto-load credentials from `process.env` when not passed explicitly, matching the convention already in place for the other adapters. `googleDrive()` reads `GOOGLE_DRIVE_CLIENT_EMAIL` + `GOOGLE_DRIVE_PRIVATE_KEY` (service-account credentials) or `GOOGLE_DRIVE_KEY_FILE` (path to a service-account JSON), plus `GOOGLE_DRIVE_SUBJECT` for domain-wide delegation, `GOOGLE_DRIVE_ID` to target a Shared Drive, and `GOOGLE_DRIVE_ROOT_FOLDER_ID` to override the bucket root (when only `GOOGLE_DRIVE_ID` is set, `rootFolderId` defaults to the drive id so Shared Drives work with no extra config). `onedrive()` reads `ONEDRIVE_ACCESS_TOKEN` (static token) or the `ONEDRIVE_TENANT_ID` + `ONEDRIVE_CLIENT_ID` + `ONEDRIVE_CLIENT_SECRET` triple (client-credentials/app-only auth), plus `ONEDRIVE_DRIVE_ID` / `ONEDRIVE_SITE_ID` / `ONEDRIVE_USER_ID` to target a specific drive โ the existing "client-credentials needs a target" guard still applies. Explicit options continue to take precedence over env vars; missing-auth error messages now mention the env fallback names.
files-sdk@1.1.1
๐ Patch Changes
- bd31113: Fix release workflow referencing a non-existent `VERCEL_PROJECT_ID_WEB` secret; now reads `VERCEL_PROJECT_ID` to match the configured repository secret so the post-publish Vercel deploy succeeds.
files-sdk@1.1.0
๐ Minor Changes
- 510cde5: Add Akamai Cloud Object Storage adapter (`files-sdk/akamai`), formerly Linode Object Storage. Thin wrapper over the S3 adapter with Akamai defaults: endpoint derived from the `region` cluster code (`us-iad-1`, `nl-ams-1`, `fr-par-1`, the older `us-east-1`/`eu-central-1`/`ap-south-1` clusters, etc.) as `https://<region>.linodeobjects.com` and overridable, virtual-hosted-style addressing, `"Akamai error"` provider label, and `AKAMAI_ACCESS_KEY_ID` / `AKAMAI_SECRET_ACCESS_KEY` env-var fallbacks. `publicBaseUrl` accepts a public-bucket origin (`https://<bucket>.<region>.linodeobjects.com`) or a custom CNAME for unsigned URLs; otherwise `url()` returns a presigned GetObject (1-hour default).
- f40e0d3: Add Box adapter (`files-sdk/box`) for personal Box and Box Enterprise via the official `box-typescript-sdk-gen` SDK. Box files live by ID rather than by path, so the adapter walks `rootFolderId` and translates virtual keys (`docs/a.txt`) into nested Box subfolders, auto-creating intermediate folders on `upload()` and racing-recovering on `item_name_in_use`. Five auth shapes (pre-built `client`, `developerToken`, `oauth` with refresh-token seeding, `ccg` with `enterpriseId` or `userId`, and `jwt` with `configJsonString` or `configFilePath`) cover scripts, user apps, and enterprise installs; env-var fallback via `BOX_DEVELOPER_TOKEN`. Token lifecycle is handled by the SDK's built-in `Authentication` classes โ no manual refresh bookkeeping. Direct `upload()` uses single-call `uploads.uploadFile` up to 50 MB and switches to `chunkedUploads.uploadBigFile` automatically; existing leaf names route through `uploadFileVersion` (overwrite). `url()` mints a signed download URL via `getDownloadFileUrl` by default; with `publicByDefault: true`, `upload()` also calls `addShareLinkToFile` (open access) and `url()` returns the link's `download_url`; `responseContentDisposition` always throws (no override on Box URLs). `signedUploadUrl()` throws โ Box uploads require a multipart POST with both an `attributes` JSON part and the file bytes part, which fits neither the SDK's PUT-with-headers nor POST-with-form-fields shape; use `upload()` server-side or Box's UI Elements / Content Uploader for browser flows. `list()` returns immediate-children files only at `rootFolderId` (no recursion, subfolders filtered out, prefix matched client-side, offset encoded as a numeric cursor). User `metadata` and `cacheControl` throw (Box exposes file metadata via classifications and metadata templates โ drop to `raw.fileMetadata.*` if you need it).
- 54edb1b: Add DigitalOcean Spaces adapter (`files-sdk/digitalocean-spaces`). Thin wrapper over the S3 adapter with Spaces defaults: endpoint derived from `region` (`https://${region}.digitaloceanspaces.com`), virtual-hosted addressing, `"Spaces error"` provider label, and `DO_SPACES_KEY` / `DO_SPACES_SECRET` env-var fallbacks. `publicBaseUrl` accepts a Spaces CDN host (`https://${bucket}.${region}.cdn.digitaloceanspaces.com`) or a custom CNAME.
- c841bbb: Add Dropbox adapter (`files-sdk/dropbox`) for personal Dropbox and Dropbox Business via the official `dropbox` SDK. Path-addressable like OneDrive, so virtual keys map directly to Dropbox paths โ no virtual-key cache. Four auth shapes (pre-built `client`, static or callable `accessToken`, OAuth refresh-token flow with `refreshToken` + `appKey` (+ optional `appSecret`), and env-var fallback via `DROPBOX_ACCESS_TOKEN` or `DROPBOX_REFRESH_TOKEN` + `DROPBOX_APP_KEY` (+ `DROPBOX_APP_SECRET`)). Refresh tokens are exchanged at `api.dropboxapi.com/oauth2/token` and cached until ~60s before expiry. `url()` mints a 4-hour temporary link via `filesGetTemporaryLink` by default; with `publicByDefault: true`, `upload()` also creates a public shared link and `url()` returns it (rewritten to `?dl=1` for direct download); `expiresIn` is capped at Dropbox's 14400s (4h) maximum and `responseContentDisposition` always throws (no override on Dropbox links). `signedUploadUrl()` throws โ Dropbox's temporary upload link expects POST with a raw body, which fits neither the SDK's PUT-with-headers nor POST-with-form-fields shape; use `upload()` or drop to `raw.filesGetTemporaryUploadLink(...)`. Direct `upload()` uses single-call `filesUpload` up to 150 MB and switches to `filesUploadSession*` (chunked, up to 350 GB) automatically; user `metadata` and `cacheControl` throw (Dropbox files have no native arbitrary-metadata field โ use `raw` with `property_groups` if you need it).
- 5ff9d79: Add Google Drive adapter (`files-sdk/google-drive`) via the official `@googleapis/drive` v3 client. Drive has no native key field, so the adapter maps virtual keys onto `appProperties.fsdkKey` and amortizes lookups with a per-instance LRU cache (configurable via `fileIdCacheSize`, defaults to 1024). Three auth shapes: inline service-account `credentials`, a `keyFilename` JSON path, or 3-legged `oauth` refresh tokens โ plus a pre-built `client` escape hatch (note: `signedUploadUrl()` requires an auth handle and throws when constructed via `client`). `signedUploadUrl()` initiates a Drive resumable session and returns the session URL as a one-shot PUT (`maxSize` is forwarded as `X-Upload-Content-Length` advisory only; `minSize` is ignored). `url()` requires `publicByDefault: true` (grants `anyone, reader` on upload and returns the permanent Drive download URL); `expiresIn` ignored, `responseContentDisposition` always throws. Service-account workloads should target a Shared Drive via `driveId` to avoid the 15 GB personal quota. Caller `metadata` keys starting with `fsdk` are reserved.
- 2a84ef2: Add Hetzner Object Storage adapter (`files-sdk/hetzner`). Thin wrapper over the S3 adapter with Hetzner defaults: endpoint derived from the `region` location code (`fsn1`, `nbg1`, `hel1`) as `https://<region>.your-objectstorage.com` and overridable, virtual-hosted-style addressing, `"Hetzner error"` provider label, and `HCLOUD_ACCESS_KEY_ID` / `HCLOUD_SECRET_ACCESS_KEY` env-var fallbacks. `publicBaseUrl` accepts a custom CNAME or proxy host for unsigned URLs; otherwise `url()` returns a presigned GetObject (1-hour default).
- b4fd387: Add Netlify Blobs adapter (`files-sdk/netlify-blobs`). Wraps the `@netlify/blobs` SDK with site-scoped or deploy-scoped stores, configurable consistency, and a metadata round-trip that packs `contentType`/`size`/`lastModified`/`cacheControl` plus user metadata into Netlify's metadata map so `head()`/`download()` return rich fields. Auto-detects credentials from Netlify's runtime context (`NETLIFY_BLOBS_CONTEXT`) when available, with explicit `siteID`/`token` overrides falling back to `NETLIFY_SITE_ID` / `NETLIFY_API_TOKEN` / `NETLIFY_BLOBS_TOKEN`. `copy()` is read-then-write since Netlify has no native copy primitive; `list()` returns key + etag (rich metadata requires a per-item `head()`); `url()` and `signedUploadUrl()` throw because Netlify Blobs has no public URL or presigned-upload primitive.
- 0d5af66: Add OneDrive adapter (`files-sdk/onedrive`) for OneDrive personal, OneDrive for Business, and SharePoint document libraries via Microsoft Graph (`@microsoft/microsoft-graph-client` + `@azure/identity`). Path-addressable like the underlying API, so virtual keys map onto real OneDrive paths โ no virtual-key cache, no reserved-metadata namespace. Four auth shapes (`clientCredentials` for app-only, `oauth` for delegated refresh-token flow, `accessToken` for caller-managed tokens, and a pre-built `client` escape hatch) and four drive targets (`/me/drive`, `driveId`, `siteId`, `userId`). `signedUploadUrl()` returns a Graph upload-session URL (one-shot PUT, advisory `maxSize`/`minSize`); `url()` requires `publicByDefault: true` and creates an anonymous-view share link (Graph has no signed URL primitive, `expiresIn` ignored). `copy()` polls Graph's async copy monitor with a configurable `copyTimeoutMs`. Direct `upload()` is capped at OneDrive's 250 MB simple-upload limit; user `metadata` and `cacheControl` throw (Graph drive items have no native arbitrary-metadata field โ use `raw` for Open Extensions).
- + 2 more
๐ Patch Changes
- 0ec97d0: Extract shared adapter helpers into `src/internal/core.ts` so authoring a new adapter is less boilerplate. The new module exports `DEFAULT_URL_EXPIRES_IN`, `joinPublicUrl`, `resolveUrlStrategy` (the two-state public-vs-sign decision, with `responseContentDisposition` always forcing signing), `normalizeBody` (Body โ `Uint8Array | ReadableStream<Uint8Array>` + content-type/length), and `makeErrorMapper` (factory for the per-provider `mapXError` scaffold โ code-set lookup, HTTP-status fallback, `FilesError` pass-through). The s3, azure, gcs, supabase, r2, fs, and uploadthing adapters now consume these helpers; supabase keeps its own `normalizeBody` because Blob pass-through is required for multipart uploads, and r2's `url()` keeps its three-state hybrid logic. `mapS3Error` retains its 2-arg legacy signature for the S3-compatible wrappers (R2 HTTP, MinIO, DigitalOcean Spaces, Storj, Hetzner, Akamai). No public-API changes.
- 30d3634: Improve test coverage and remove dead code in the fs adapter. Adds tests for r2's HTTP-path delegation (copy/delete/download/head/list/signedUploadUrl proxies to the lazy-loaded inner s3 adapter, plus the `raw` getter's pre/post-init behavior) and for fs uploads with `ArrayBuffer` and `ArrayBufferView` bodies plus rejection of keys that resolve to the adapter root. Drops the unreachable `ReadableStream` branch in `fs/bodyToBytes` โ stream uploads route through `writeStreamToTempThenRename`, so the parameter type is narrowed to `Exclude<Body, ReadableStream<Uint8Array>>` to enforce that at the type level.
files-sdk@1.0.0
๐ Major Changes
- 30900e6: Initial release
