---
changelog:
  category: Release
  version: files-sdk@1.8.0
date: '2026-06-07T01:30:05Z'
seo:
  description: >-
    Add a compression() plugin at files-sdk/compression for transparent, at-rest
    compression. Bodies are gzipped (or deflate / deflate-raw) on upload with
    the…
title: files-sdk@1.8.0
type: changelog
---
### 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({
    adapter: s3({ bucket: "uploads" }),
    plugins: [
      {
        name: "uppercase",
        wrap: handlers({
          upload: (op, next) =>
            next({ ...op, body: (op.body as string).toUpperCase() }),
        }),
      },
    ],
  });
  ```

  Each plugin offers two optional capabilities: `wrap` (intercept any operation via the `next` onion) and `extend` (add methods like `files.usage()`). Ships with the `handlers()` helper for authoring per-verb `wrap`s with automatic passthrough, and the `createFiles()` factory that surfaces `extend` methods on the instance type. Plugins run inside the `onAction`/`onError` hooks but outside retry and key prefixing, and intercept both single and bulk operations.

- 79e0104: Add a `tracing()` plugin at `files-sdk/tracing` for OpenTelemetry spans around every operation. Each call opens one span named `files.<verb>` carrying the caller-facing key (or `from` / `to` for `copy` / `move`), a `files.bulk` flag for batch items, and a cheap result attribute on success (`files.size`, `files.exists`, `files.count`); a throw is recorded with `recordException` and an `ERROR` status, then re-thrown untouched. Spans are opened with `startActiveSpan`, so each op span nests under your active request span and the sub-operations inner plugins issue nest beneath it in turn. `@opentelemetry/api` is an **optional peer dependency**: the tracer defaults to the global `trace.getTracer("files-sdk")` (a no-op until you register an OpenTelemetry SDK), or pass your own to scope the instrumentation name/version. Tune span names with `spanPrefix` and attach or redact attributes with `attributes(op)` (return `{ "files.key": undefined }` to keep sensitive keys out of traces). It's body-transparent (sizes come from declared metadata, never the bytes, so streaming / ranges / `url()` keep working), counts one span per logical operation rather than per retry attempt, and opens a span per item of a bulk call. Place it first (outermost) to span the caller-facing operation with inner-plugin work nested beneath, or last to time only the provider call.
- 60f3b63: Add a `usage()` plugin at `files-sdk/usage` for metering storage, bandwidth, and operation counts. It tallies every operation on a `Files` instance and surfaces the running totals via `files.usage()`: each call counts as one operation (with a per-verb `operationsByKind` breakdown), `upload` adds its result size to `bytesUp`, and `download` / `head` wrap the returned body so the bytes you actually read add to `bytesDown` — metered lazily, chunk-by-chunk, so an unread body costs nothing and a fire-and-forget hook couldn't do it. Pass `{ group }` to bucket usage per tenant or prefix and read it back with `usageByGroup()`; `resetUsage()` starts a fresh window. It's body-transparent (no buffering, no metadata, no native deps, so streaming / ranges / `url()` keep working), counts logical operations rather than retry attempts, and counts each item of a bulk call. Place it first (outermost) to meter logical bytes and caller-facing operations, or last to meter bytes-on-the-wire to the provider. Construct with `createFiles` so `files.usage()` shows up on the type.
- 8c68c34: Add a `validation()` plugin at `files-sdk/validation` — a fail-closed guard that vets writes before any bytes reach the adapter. Enforce a max/min size, an allowed-MIME-type list (exact or `type/*`), and a key-naming rule (a `RegExp` or predicate); the key rule also guards the destination of `copy`/`move`. It transforms nothing and stores no metadata, so reads, `url()`, `copy`, and `move` pass straight through, while `signedUploadUrl()` fails closed when a size or type rule is set (a presigned upload bypasses the plugin). No native dependencies; works on any adapter.
- 3cecd4c: Add a `versioning()` plugin at `files-sdk/versioning` that snapshots an object's prior bytes before any overwrite or delete and adds `files.versions(key)` / `files.restore(key, versionId?)` to roll a key back. Snapshots are server-side copies under a configurable prefix (`.versions/` by default), so it's body-transparent — streaming, range downloads, `url()`, and `signedUploadUrl()` keep working, and it composes with `compression()` / `encryption()` by snapshotting whatever they stored. Optional `limit` caps the versions kept per key; version objects are hidden from `list()`. It's the first plugin to use `extend`, so use `createFiles` to surface the new methods on the type. No native dependencies; works on any adapter.

### 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.
