files-sdk@1.8.0
Minor Changes
-
87607ec: Add a
compression()plugin atfiles-sdk/compressionfor 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 atfiles-sdk/content-typethat decides an upload'sContent-Typefrom its bytes instead of the client's claim. It magic-byte-sniffs the body onuploadand either corrects the stored type to match (the default) or rejects a mismatch, so a.pngwhose 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 exportsdetectContentType(). No native dependencies; works on any adapter. -
5ad680e: Add a
dedup()plugin atfiles-sdk/dedupfor content-addressed de-duplication. Onuploadthe 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, andcopy/moveof a de-duplicated file is near-free and shares the blob. Reads are transparent —downloadfollows the pointer (ranges included, since blobs are stored verbatim), andhead/listreport 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 beforecompression()/encryption()in the array — encrypted bytes don't de-dup. -
feaf806: Add an
encryption()plugin atfiles-sdk/encryptionfor 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 exportsgenerateEncryptionKey(). -
4d40229: Add a
files.search(pattern, options?)method that finds objects whose key matches a pattern. By defaultpatternis 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); setmatchto"regex","substring", or"exact", or pass aRegExpdirectly, to change that. It returns a streaming async iterable ofStoredFilebuilt onlistAll, so it walks every page lazily (stays memory-bounded,breakormaxResultsto stop early) and works on every adapter with no per-provider capability. A glob's literal prefix is pushed down to the underlyinglistautomatically (uploads/2024/*.pdfscopes the walk to theuploads/2024prefix); for a regex/substring/case-insensitive search, passprefixto bound the walk. The CLI gains afiles search <pattern>command (--match/--regex/--prefix/--limit/--max-results/--case-insensitive) and the MCP server asearchtool. -
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 ofhooks) — 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 thenextonion) andextend(add methods likefiles.usage()). Ships with thehandlers()helper for authoring per-verbwraps with automatic passthrough, and thecreateFiles()factory that surfacesextendmethods on the instance type. Plugins run inside theonAction/onErrorhooks but outside retry and key prefixing, and intercept both single and bulk operations. -
79e0104: Add a
tracing()plugin atfiles-sdk/tracingfor OpenTelemetry spans around every operation. Each call opens one span namedfiles.<verb>carrying the caller-facing key (orfrom/toforcopy/move), afiles.bulkflag for batch items, and a cheap result attribute on success (files.size,files.exists,files.count); a throw is recorded withrecordExceptionand anERRORstatus, then re-thrown untouched. Spans are opened withstartActiveSpan, so each op span nests under your active request span and the sub-operations inner plugins issue nest beneath it in turn.@opentelemetry/apiis an optional peer dependency: the tracer defaults to the globaltrace.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 withspanPrefixand attach or redact attributes withattributes(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 atfiles-sdk/usagefor metering storage, bandwidth, and operation counts. It tallies every operation on aFilesinstance and surfaces the running totals viafiles.usage(): each call counts as one operation (with a per-verboperationsByKindbreakdown),uploadadds its result size tobytesUp, anddownload/headwrap the returned body so the bytes you actually read add tobytesDown— 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 withusageByGroup();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 withcreateFilessofiles.usage()shows up on the type. -
8c68c34: Add a
validation()plugin atfiles-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 ortype/*), and a key-naming rule (aRegExpor predicate); the key rule also guards the destination ofcopy/move. It transforms nothing and stores no metadata, so reads,url(),copy, andmovepass straight through, whilesignedUploadUrl()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 atfiles-sdk/versioningthat snapshots an object's prior bytes before any overwrite or delete and addsfiles.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(), andsignedUploadUrl()keep working, and it composes withcompression()/encryption()by snapshotting whatever they stored. Optionallimitcaps the versions kept per key; version objects are hidden fromlist(). It's the first plugin to useextend, so usecreateFilesto 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
wrapcallsnext()with a different verb than the one it's intercepting — e.g.dedup()'sexistsprobe, orversioning()'s snapshothead+copy— misrouted when it ran insideupload([...])/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 andexportsmap 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.tsfiles; type resolution for consumers is equivalent.