Skip to content

files-sdk@1.8.0

Choose a tag to compare

@github-actions github-actions released this 07 Jun 01:30
· 52 commits to main since this release
Immutable release. Only release title and notes can be modified.
13bf637

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.

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