Changelog
July 6, 2026
Minor Changes
- 6ee0980: Add
files-sdk/tanstack-startserver adapter —createRouteHandlermounts the Files gateway in a TanStack Start server route (server.handlers). - 5f7c09b: Add a WebDAV adapter (
files-sdk/webdav) backed by thewebdavclient. HTTP-based, with native server-sidecopy/move, ranged and streaming downloads, and content-type round-tripping. Works against Nextcloud, ownCloud, Apachemod_dav,sabre/dav, and other WebDAV servers. Available in the CLI and MCP as thewebdavprovider.
Patch Changes
- 77ae3a8: Reject Cloudinary signed upload URLs when a server-enforced max size is requested.
- 8d4e235: Disable Convex signed upload URLs because generated upload capabilities cannot bind SDK keys or upload limits.
- 283f658: Remove public fallback upload-token secrets from the docs demo routes.
- 4ea52ce: Cap public docs demo uploads on the
/api/filesin-memory route. - 763bc47: Cap public docs demo uploads on the
/api/files-trashin-memory route. - 79c8c60: Cap public docs demo uploads on the
/api/files-versionsin-memory route. - 63607ba: Disable fs signed upload URLs because the adapter cannot signature-bind upload constraints.
- dd12299: Reject filesystem adapter reads when symlinks resolve outside the configured root.
- 8a0bf64: Bind MCP transfer and sync destinations at server startup instead of accepting provider config from tool calls.
- 8d396d2: Enforce approval-gated OpenAI Responses write tool calls inside execute().
- 6d29787: Encode public URL dot segments so keys cannot escape configured base paths.
- 0703817: Validate persisted resumable upload session URLs before resuming provider uploads.
- d54f272: Honor router authorization maxResults when serving list requests.
- 92e5700: Require same-origin requests by default for state-changing files router operations.
- 52bf6f9: Enforce router upload byte limits while streaming bodies without content-length headers.
- 2fc4d87: Force safe attachment dispositions for gateway URL requests unless server authorization overrides them.
- 9c41388: Assemble the search ReDoS test pattern at runtime to clear a CodeQL
js/redosfalse positive; no runtime behavior change. - a33b424: Reject unsafe regular expressions in search APIs before walking provider keys.
- f1f0012: Match stateful (
g/y)RegExpsearch patterns against every key independently, and rebuild caller-supplied regexes so search never mutates the original instance. - f1891ad: Clamp router signed upload URL size limits to the configured max upload size.
- 737f82f: Pin signedUrlPolicy upload URL expiry to maxExpiresIn when runtime callers omit expiresIn.
- 6f5ea66: Apply router
filterKeysauthorization to soft-delete purge operations. - 5bbf406: Reject UploadThing signed upload URLs when a server-enforced max size is requested.
- ee4f6f9: Limit ZIP extraction entry counts and decompressed sizes before uploading entries.
June 21, 2026
Major Changes
- 2b81046: Release files-sdk 2.0 — the full-stack release. Alongside the core
FilesAPI, the SDK now reaches the browser with framework client bindings (files-sdk/react,files-sdk/vue,files-sdk/svelte), a server gateway (files-sdk/api) with route handlers for Next.js, Hono, Express, Fastify, Koa, Elysia, Nitro, SvelteKit, Astro, Bun, and Deno, and a shadcn UI component registry wired to theuseFileshook. See the accompanying changesets for the full surface.
Minor Changes
- 58757d7: Add an Archil adapter (
files-sdk/archil) for Archil disks over their S3-compatible API. The disk id is the path-style bucket and the endpoint is derived from the Archil region; SigV4 enables byte ranges, multipart, and presigned URLs. Supports abranchoption (branch-scoped access) and an optionaldiskinstance exposed atadapter.diskfor Archil-native operations. - 31c9c3f: Add
files-sdk/api—createFilesRouter, a server gateway exposing the wholeFilesverb set (upload, download, head, exists, list, search, url, delete, copy, move, capabilities, signed upload URLs) over a single endpoint, with deny-by-default per-operationauthorize(throw to deny, return a key-prefix/expiry/read-only constraint), redirect-or-proxy streaming downloads (Range/206 + client-disconnect abort), keyless presign→complete uploads with a proxy fallback, HMAC round-trip tokens, and an origin allowlist. - 91d20ef: Add
files-sdk/astro—createRouteHandler(router)returns{ GET, POST, PUT }for an Astro endpoint (GETserves downloads,POSTthe JSON verbs,PUTthe upload byte path). The handlers are Web-native, so the route runs on Node and edge adapters alike. The endpoint must run per-request: setprerender = false(oroutput: "server") with an SSR adapter. - 31c9c3f: Add
files-sdk/client—createFilesClient, a framework-agnostic verb client for the gateway;downloadreturns the same lazyStoredFilethe server SDK returns. - 31c9c3f: Add
files-sdk/express—createRouteHandler(router)returns a Node(req, res)handler that bridgesIncomingMessage/ServerResponseto the WebRequest/Responsethe gateway speaks (also works with Connect and a rawhttp.createServer). A client disconnect aborts the upstream read on a proxied download. Mount it before any body parser so the gateway can read the raw upload/JSON body. - 91d20ef: Add
files-sdk/fastify—createRouteHandler(router)returns a Fastify(request, reply)handler thatreply.hijack()s and bridges the rawIncomingMessage/ServerResponseto the WebRequest/Responsethe gateway speaks (the same seam asfiles-sdk/express). A client disconnect aborts the upstream read on a proxied download. Drop Fastify’s built-in body parsers (removeAllContentTypeParsers()+ a no-opaddContentTypeParser("*", …)) so the gateway can read the raw upload/JSON body. - 31c9c3f: Add
files-sdk/hono—createRouteHandler(router)returns a single Hono handler (app.all("/api/files", handler)). Web-native, so it runs on Workers, Bun, Deno, and Node. - 91d20ef: Add
files-sdk/koa—createRouteHandler(router)returns a Koa handler that setsctx.respond = falseand bridgesctx.req/ctx.resto the WebRequest/Responsethe gateway speaks (the same seam asfiles-sdk/express). A client disconnect aborts the upstream read on a proxied download. Mount it before any body parser so the gateway can read the raw upload/JSON body. - 31c9c3f: Add
files-sdk/next—createRouteHandlerto mount the gateway in the Next.js App Router. - 91d20ef: Add
files-sdk/nitro—createRouteHandler(router)returns an h3 event handler for Nitro (and Nuxt server) routes that marshalsevent.node.reqinto the WebRequestthe gateway speaks and returns the WebResponsefor Nitro to flush, hiding thetoWebRequest(event)step. A client disconnect aborts the upstream read on a proxied download. Targets Nitro v2 / h3 v1, whereevent.node.reqis present on every preset. - 9923947: Expose the
versioning()andsoftDelete()plugin verbs through the gateway, client anduseFileshook.createFilesRouternow dispatchesversions/restoreVersion/trashed/restoreTrashed/purge(each a new deny-by-defaultFilesOperation, answered only when the matching plugin wraps theFilesinstance — otherwise a 422), andcreateFilesClient/useFilesgain matching methods (files.versions(key),files.restoreVersion(key, versionId?),files.trashed(),files.restoreTrashed(key),files.purge(key?)). Trash listing and “empty trash” are key-prefix-scoped, so a multi-tenantauthorizekeyPrefix never leaks or purges another tenant’s trash. - 31c9c3f: Add
files-sdk/react—useFiles({ endpoint })returning every verb (imperative, with ambient upload progress/error) plus optional reactiveuseList/useFile/useSearchhooks. Emitted as a"use client"module. - 31c9c3f: Add
files-sdk/svelte— the Svelte binding:useFilesreturning Svelte stores for the ambient state, plususeList/useFile/useSearchquery stores. Store-based (no Svelte runtime dependency). - 91d20ef: Add
files-sdk/sveltekit—createRouteHandler(router)returns{ GET, POST, PUT }for a SvelteKit+server.tsendpoint (GETserves downloads,POSTthe JSON verbs,PUTthe upload byte path). The handlers are Web-native, so the route runs on the Node and edge adapters alike. This is the server binding, distinct from thefiles-sdk/svelteclient store. - b20440c: Add a shadcn component registry of
useFiles-wired UI, installable withnpx shadcn add. Upload + display:dropzone,file-list,file-preview,upload-progress,multipart-uploader. Navigation + actions:file-browser(folder tree vialist({ delimiter })+ breadcrumbs),file-search(search()with glob/regex/substring/exact),share-dialog(url()/signedUploadUrl()with expiry + copy),file-actions(copy/move/rename/download/delete menu),capabilities-badges(capabilities()as feature badges). Plugin showcases:version-history(versioning()— list + restore snapshots) andtrash-bin(softDelete()— restore + purge soft-deleted files). The components ship in the docs site rather than the package, but they’re a first-class part of the SDK surface. - 31c9c3f: Add
files-sdk/vue— the Vue 3 twin of the React hook: auseFilescomposable returning refs for the ambient state, plus reactiveuseList/useFile/useSearchcomposables over the same gateway.
June 14, 2026
Minor Changes
- ff814cc: Add an
audit()plugin atfiles-sdk/auditthat writes a structured who/what/when record of every mutation to an awaited sink — the durable, awaitable counterpart to the fire-and-forgetonActionhook. Each audited operation produces oneAuditRecordcarrying the verb, the caller-facing key (orfrom/to), an optionalactor, the start time and duration, the outcome, and — on a successfulupload— 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); passevents: "all"to also audit reads, or an explicit list to record exactly the verbs you name. Resolveactorsynchronously from your request context to attribute each record. It’s body-transparent (never buffers, transforms, or reads the body —sizecomes 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; bulkupload([...])/delete([...])fan out to one record per item, each flaggedbulk: true. It’swrap-only, so plainnew Files({ plugins })works. Place it first (outermost) so it records the caller’s logical intent — adeletean innersoftDelete()turns into amoveis still audited as thedeletethe caller asked for. - daca585: Add a
cache()plugin atfiles-sdk/cache— an LRU/KV cache in front of the cheap read verbs. A repeathead()orurl()(and, opt-in, a smalldownload()) 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.headcaches metadata only (a hit’s body still lazy-fetches on access, matching the uncachedheadcontract);urlcaches per url-options signature and caps each entry at its ownexpiresInso a presigned URL is never handed out past its signature;downloadis off by default and, when enabled viaoperations: ["download"], buffers only known-length bodies at or undermaxBytes(default 1 MiB) so streaming and large objects keep working. Defaults to a bounded in-memory LRU (maxEntries, default 1000), or pass your ownCacheStoreto back it with a shared KV. Entries honor attl(default 60s;0disables 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 usesextendforinvalidateCache(key?),cacheStats(), andresetCacheStats()— construct withcreateFilesto 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 callinvalidateCache()and treat the cache as eventually-consistent. - 83d6eb4: Add a
failover()plugin atfiles-sdk/failoverthat 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 insecondaries(a singleAdapteror an array for a multi-region chain), each wrapped in its own internalFilesso it gets the same retry, capability gating, andStoredFilenormalization. Every verb runs the same way: try the primary; if it throws andshouldFailoversays 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 onProvidererrors (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 ownshouldFailoverto widen it (e.g. read through to a replica onNotFound) or narrow it. This is the availability counterpart totiering()(which partitions by key/size): failover treats each secondary as a full replica, so it never splits or merges across backends —listreturns 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’sreplication()). A streamingupload(aReadableStreambody) can’t be replayed, so it runs against the primary alone and isn’t failed over. An optionalonFailovercallback (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 (wraponly), so it works with plainnew Files({ plugins }). Place it last (innermost) so body-transforming plugins likeencryption()wrap every backend, and give each secondary its own bucket / container (secondaries receive caller-facing keys, without the instanceprefix). Failover buys availability, not convergence — reconcile a secondary written during an outage withsync/transfer, or keep it current withreplication(). - 581c97f: Add a queryable
files.capabilitiessurface 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 anAdapterCapabilitiessnapshot with eight fields, each mirroring an operation the unified API actually exposes:rangeRead,uploadProgress,delimiter,metadata,cacheControl, andmultipartare derived live from the same per-adapter flags and optional methods the wrapper already gates on (so they can never drift from runtime behavior), whileserverSideCopyandsignedUrl({ 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.supportedistruewhenurl()can mint a signed or tokenized URL (not just a permanent public link);maxExpiresInis set only where a provider enforces a hardexpiresInceiling 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 optionalsupportsServerSideCopyandsignedUrlfields alongside the existingsupports*flags; both are advisory and gate nothing. See the new Capabilities and Provider gaps documentation. - 81e0e64: Add a
neonadapter atfiles-sdk/neonfor Neon 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 standardAWS_*variables thatneon dev/neon env pullinject for the linked branch —endpointfromAWS_ENDPOINT_URL_S3, region fromAWS_REGION(thenNEON_STORAGE_REGION, thenus-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 infiles-sdk/providersand exposed through the CLI. - 2275982: Add an opt-in
receiptsoption that surfaces a provenanceReceiptfor 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 withreceipts: trueto attach aReceipt({ op, provider, key, bytes?, etag?, sha256?, durationMs, ts }) to the successonActionevent of each mutating call — an additivereceiptfield on the existing hook, with no new operation, callback, or changed return type. Every field exceptsha256is derived from the work the SDK already does for the hook (timing, the adapter name, the caller-facing key, andbytes/etagread straight off theUploadResult), so plainreceipts: trueadds no per-call cost.sha256is the one field with a real per-call cost and is opt-in by name: passreceipts: { sha256: true }to fingerprint the upload body as passed toupload()— taken before any plugin transform, so it matches whatdownloadgives back rather than the (possibly encrypted/compressed) bytes on disk — a lowercase-hex SHA-256, present only on anuploadof a buffered body. A streaming upload is never buffered to hash it (so it carries no fingerprint), anddelete/copy/movetransfer no content of their own; withsha256off, the body is never read. Reads,signedUploadUrl, failures, bulk array calls, and receipts-off instances all leaveevent.receiptunset. See the new Receipts documentation. - 5a77a58: Add a
signedUrlPolicy()plugin atfiles-sdk/signed-url-policythat enforces safe defaults on the two URL-minting operations, turning the security caveatsurl()andsignedUploadUrl()document into the default. Onurl()it forces a downloadContent-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 existingattachment(and itsfilename), and clampsexpiresIntomaxExpiresIn. OnsignedUploadUrl()it clampsexpiresInto the same cap and, whenmaxUploadSizeis set, guarantees a server-enforcedmaxSizeis 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()forcesattachment). Setdisposition: falseto 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 atfiles-sdk/soft-deletethat turnsdeleteinto a recoverable move into a trash prefix - a recycle bin for any adapter. Instead of destroying an object, adeleteserver-side moves it to"<prefix>/<key>"(.trash/by default); the bytes only leave storage when youpurge(). It adds three methods viaextend(so construct withcreateFiles):trashed()lists what’s in the trash (each entry carries the originalkeyplus a downloadabletrashKey),restore(key)moves the trashed copy back over the live key (overwriting a re-created one, throwing when nothing’s trashed), andpurge(key?)permanently deletes one item or empties the whole trash (idempotent). Likeversioning()it’s body-transparent - it never buffers, transforms, or reads the body, so streaming, range downloads,url(), andsignedUploadUrl()all keep working - and has no native dependencies. Trashed objects are hidden fromlist()(unless you list within the prefix); adeleteof a key inside the trash prefix is a real delete (that’s howpurge()works); deleting a missing key stays a no-op; and bulkdelete([...])soft-deletes every key. One trashed copy is kept per key (re-deleting replaces it - reach forversioning()to keep every generation). Place it first (outermost) so it relocates whatever the rest of the pipeline stored. - ce69a47: Add a
tiering()plugin atfiles-sdk/tieringthat routes operations between a hot and a cold adapter by size, prefix, or age. The hot tier is the instance’s own adapter (reached through the rest of the onion); the cold tier is a second adapter passed incold(wrapped in its own internalFiles, so it gets the same retry, capability gating, andStoredFilenormalization). A requiredroute({ key, size? })function decides each operation’s tier —sizeis the body’s declared length onupload(when known), and omitted everywhere else.uploadlands in the routed tier;download/head/url/existsconsult it;deleteremoves it;copy/movelocate the source, route the destination by key, and either use a native same-tier op or stream the bytes across when the tiers differ;listmerges a page from each tier (keys sorted within a page) and paginates the two independently via a composite cursor;signedUploadUrlsigns against the routed tier. Withfallback: true, an object’s tier is treated as discoverable rather than fixed — reads fall through to the other tier on a miss,deleteclears both, and anuploadevicts the other tier so exactly one copy exists; turn it on forsize-based routing or when you move objects with the new methods. It adds two methods viaextend(so construct withcreateFiles):tierOf(key)reports which tier holds a key, andtier(key, target)streams an object across tiers (the lever for age-based transitions — list, checklastModified, then tier it down). It’s body-transparent (a cross-tier copy streams, never buffers) and has no native dependencies. Place it last (innermost) so body-transforming plugins likeencryption()wrap both tiers, and address objects by caller-facing keys (the cold adapter doesn’t receive the instanceprefix— give it its own bucket / container). - 649ac09: The
validation()plugin now throws a dedicatedValidationError(exported fromfiles-sdk/validationalong with theValidationReasontype) with areasondiscriminant —"size","type", or"key"— so callers can branch on which rule failed without parsing the message. It’s backward compatible:ValidationError extends FilesError, keepscode: "Provider", and the messages are unchanged, so existing catches keep working.maxSize/minSizesharereason: "size"(the message says which bound), and thesignedUploadUrl()fail-closed throw stays a plainFilesError— it’s the plugin refusing an unenforceable operation, not the file failing a rule. - 6aca1e5: Add a
zip()plugin atfiles-sdk/zipfor bundling stored objects into ZIP archives and back out of them. Anextend-only (Tier C) plugin contributing three methods:files.zip(selection)streams many keys as one standard ZIP archive (entries download lazily one at a time, so memory stays flat — pipe it straight into aResponse),files.zipTo(key, selection)stores that archive back as an object, andfiles.unzip(key, { into })extracts an archive’s entries into individual objects with content types inferred from their extensions. A selection is an explicit key array or{ prefix }(resolved vialistAll);method: "store" | "deflate"picks the compression (deflate via the platformCompressionStream— no native deps, works on any adapter) andname(key)remaps entry paths. Everything runs through the fully-wrapped instance, so it composes withencryption()/compression()transparently. Classic ZIP only (no ZIP64: 65,535 entries / 4 GiB caps fail closed), entry names are validated on both sides (duplicates,..zip-slip segments, absolute paths), extraction verifies CRC-32/size and refuses encrypted entries and unknown methods, andunzipbuffers the whole archive (the central directory lives at the end) whilezipstreams.
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
commitBlockListfail loudly (InvalidBlockList) rather than committing with gaps, and a retry re-probes correctly. Comment-only; no behavior change. - bcad8b4: Fix untyped
Blob/Fileuploads being sent with an emptyContent-Type.Blob.typeis""(never nullish) when no type was given, so the documentedapplication/octet-streamfallback behind a??was dead code — the provider receivedcontentType: "". 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/deleteignoring the constructor-levelsignalandtimeoutdefaults. 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 configuredtimeoutnever 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/
undefinedarray slot. The per-worker guardreturned instead of skipping the slot, so withconcurrency: 1(or as many holes as workers) every key after the hole was silently neither processed nor reported inresults/errors. Only reachable past the type system (a sparse array or anundefinedelement cast in), but the recovery is now to skip just that slot. - 56965b4: Fix
cache()serving presigned URLs past their signature whenurl()is called withoutexpiresIn. The signature-lifetime cap only applied when the caller passedexpiresIn, but the adapter signs default calls with a finite lifetime too — so with a longttl(orttl: 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 newdefaultUrlExpiresIncache 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
errorsarrays embed liveFilesErrorinstances, and a bareJSON.stringifydropsmessage(a non-enumerableErrorproperty) while serializing the enumerablecause— 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 embeddedFilesErrorand stripscause. - 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 bulkheadwith errors) could be cut off mid-JSON before the consumer received it. Commands now signal failure viaprocess.exitCodeand 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 — sofiles --helpcrashed withERR_MODULE_NOT_FOUNDunless every optional peer was installed. The build now emits shared chunks so the registry’sawait 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. - 4c66027: Fix the CLI blaming
--config-jsonfor malformed JSON passed totransfer --to/sync --to. The shared JSON parser hardcoded the flag name in its error message; it now names the flag the user actually passed. - ee52de2: Fix the CLI silently swallowing non-EPIPE stdout errors. The EPIPE-as-success handler (for
files … | head-style pipelines), by being registered, also suppressed Node’s default throw for every other stdout error — an EIO/EBADF or a full disk behind a redirect let the command exit 0 having written nothing. Non-EPIPE stdout errors now report to stderr and exit 2. - 9cd61f4: Fix CLI integer flags silently truncating trailing garbage.
--part-size 5MBparsed to 5 bytes,--timeout 1sto 1 millisecond, and--limit 1.9to 1 —parseIntonly rejected fully non-numeric input. Integer flags now require a plain integer and fail loudly otherwise. - 3584ab9: Fix
contentType()leaving the caller’s source stream locked and open when a stream upload is rejected. WithonMismatch: "reject"/onUnknown: "reject"(or any downstream failure before the replay body was consumed), the peek reader held its lock forever and the underlying request body / file handle was never cancelled. The replay body is now cancelled best-effort when the upload throws. - e904016: Correct the Dropbox adapter’s
expiresIndocumentation.filesGetTemporaryLinktakes no expiry parameter — every temporary link lives ~4 hours regardless of what’s requested — but the docs claimedexpiresInwas “honored up to the 4h cap”. It is validated only (values above 14400s throw); a shorterexpiresInis accepted but the link outlives it, so it must not be relied on as a security control with this adapter. - 024946a: Harden
encryption()against envelope-metadata tampering and document its threat model precisely. GCM already authenticates the ciphertext and the wrapped DEK, butfsenc_size— the declared plaintext size thathead()/list()report — is plain metadata an attacker with raw provider write access could forge;download()now verifies it against the decrypted length and throws on a mismatch. The JSDoc now also states explicitly that the envelope is not bound to its object key (an attacker with raw provider write access can splice a whole envelope onto another key): binding to keys would break the documented server-sidecopy/moveand key-aliasing plugin compositions, so tenants needing that isolation should use separate KEKs. - fcc252e: Fix
failover()never failing over on timeouts. The docs promised the default predicate covers “network failures, timeouts, and 5xx”, but a per-attempttimeoutsurfaces as anabortederror, which the predicate excluded — so a hung primary (the canonical case the plugin exists for) surfaced the timeout instead of trying the secondary.FilesErrornow carries atimedOutflag (set only by the configuredtimeout, never by a caller’s abort signal), and the default predicate fails over on timeouts while still respecting deliberate caller aborts. - 781aecc: Harden the fs adapter’s resumable-upload
adopt()against doctored resume tokens. The persisted token’stempPathwas adopted verbatim, so a tampered token (e.g. one stored in Redis/a DB and rehydrated viaUploadControl.from) could point the partial-file writes, the completing rename, and the discard delete at an arbitrary filesystem path outside the adapter root. The temp path is fully derived from the traversal-checked key, so it is now recomputed and a token whosetempPathdoesn’t match is rejected — which also catches tokens minted against a different adapter root. - 9188022: Fix silent file corruption in FTP/SFTP resumable uploads when a chunk is retried.
uploadAt()appended at the server-side EOF without consulting the chunk’soffset, so a per-chunk retry after a partial append — or after a lost success reply — appended the chunk again, leaving duplicated bytes in the middle of the file while the upload “succeeded”. The drivers now verify the remote size matches the expected offset before appending and, on a mismatch, skip the write and report the server’s real offset so the orchestrator re-slices from there. - c4b1426: Fix the Google Drive adapter creating a duplicate file on every overwrite. Drive has no unique-name constraint and the adapter always called
files.create, so uploading an existing key a second time left two files carrying the same virtual key — from then on everyhead/download/delete/urlon that key from a fresh instance threwConflict(the writer’s own id cache masked it). Writes now look the key up first:upload()updates the existing file in place,copy()deletes the clobbered destination file after a successful copy, and resumable uploads /signedUploadUrl()initiatePATCHupdate sessions against the existing file id instead of creating a new one. - 9ea505f: Stop retrying deterministic failures. The “server ignored the requested byte range” and “only supports the / delimiter” guards throw
Provider-coded errors from inside the retryable adapter call, andProviderwas the one code the retry loop treats as transient — so a rangeddownload()with retries against a host that ignoresRangere-issued (and re-transferred) the full GET on every attempt with backoff in between before surfacing the error.FilesErrornow carries apermanentflag that opts a deterministic failure out of retries, set by both guards. - 3ade008: Fix the offset-HTTP resumable driver (GCS/Firebase/Google Drive) optimistically advancing past a chunk on a
308response with noRangeheader. In this protocol that response means the server persisted nothing (the probe path already maps it to offset 0), so assuming the whole chunk landed silently skipped its bytes and made the upload fail later at a confusing offset. The chunk now throws a retryable error instead, so the per-chunk retry re-sends it and a token resume re-probes the true offset. - d99e757: Fix
control.abort()racing session creation in resumable uploads. Aborting whiledriver.begin()(or a resumeprobe()) was in flight found no discard hook installed yet, so the just-created provider-side session (e.g. an S3 multipart upload, billed until aborted) was never discarded — and the session assignment then re-populated a live token onto the aborted control, violatingabort()’s terminal contract. The orchestrator now notices the abort right after session setup, discards the provider session, and keeps the control terminal. - 3ade008: Fix
onProgressreportingloaded: Number.MAX_SAFE_INTEGERwhen a resumed offset-mode session had already finalized server-side. The probe signals “already done” with a past-the-end sentinel offset, which the orchestrator forwarded verbatim to progress reporting — any UI computingloaded / totalshowed a ~9·10¹⁵-byte upload. The orchestrator now clamps the starting offset to the body size; the upload still completes with the probed result as before. - 7b7c731: Fix multipart resumable uploads continuing in the background after a part fails. When one part exhausted its retries,
upload()rejected but the sibling workers kept slicing and uploading every remaining part (burning bandwidth and provider requests),onProgresskept firing after rejection, the pause gate flipped the control’s status from"error"back to"uploading", and a laterresume()could wake paused workers into the dead run. A part failure now latches the run: new dispatches stop, in-flight sibling attempts are aborted via a run-scoped signal, parked workers wake up and bail, and the control’s status stays"error". - ea7051e: Fix
softDelete()dropping the caller’s operation options on the trash move. Asignal/timeout/retriespassed tofiles.delete(key, opts)was silently ignored for the re-routed move, making the delete un-abortable and unbounded. The options now thread through. - d0061bb: Fix the Supabase adapter passing
responseContentDispositionstraight through as Supabase’sdownloadfilename. Supabase’sdownload: stringoption means “attachment named this”, soresponseContentDisposition: "attachment"served a file literally namedattachment, and a fullattachment; filename="report.pdf"value produced a garbled filename embedding the whole header. Bareattachmentnow maps todownload: true, afilename=parameter maps to that name, and dispositions Supabase can’t express (e.g.inline) throw instead of being mislabeled. - 2e4d2e2: Fix the Supabase adapter’s flat
list()missing nested objects. The no-delimiter path used the legacy V1list()API, which is folder-scoped and non-recursive — a bucket with nested keys (docs/a.txt) listed phantom zero-byte rows for the folders and never returned the nested objects, solistAll,search(),sync/transfer, and every list-based plugin silently missed them; a partial prefix (prefix: "do") returned nothing at all. The flat path now uses the V2 list API: a recursive string-prefix scan over full keys with a real server cursor. Note that flat-list cursors are now opaque V2 cursors rather than numeric offsets — don’t persist cursors across versions. - 2b8780f: Fix
tiering()’stierOf()/tier()ignoring the instanceprefix. The extend methods built their hot-tier runner from the bare adapter, while every other operation goes through the plugin chain and gets the prefix applied — so withprefixset,files.exists(key)wastruebutfiles.tierOf(key)returnedundefined, andfiles.tier(key, …)threwNotFound(or touched a same-named unprefixed object). The extend runner now re-applies the prefix.Filesalso gains a publicprefixgetter so plugins can do the same. - 9235eab: Fix
tiering()’s mergedlist()emitting a both-tier key twice across pages. The “hot wins” dedup was per page while the two tiers paginate independently, so a key present in both tiers (exactly the stale-shadow statefallbackmode anticipates after a crash mid-eviction) appeared twice — with potentially different sizes/etags — once each tier’s stream reached it, breakinglistAll/sync/searchconsumers. Merged listing is now globally key-ordered: each page emits entries only up to the lowest page boundary among tiers that still have more, holding the rest back via askipmarker in the composite cursor, which makes cross-page duplicates (of keys and of delimiter prefixes) impossible. An undecodable composite cursor now throws instead of silently restarting the listing from the top. Composite cursors changed shape — don’t carry a list cursor across versions. - dac57c2: Fix
transfer()andsync()leaking the source download stream when the destination upload fails. A destination that rejects before draining the body (auth error, rejected metadata, a fail-closed plugin) left the already-opened source stream — an HTTP response or file descriptor — neither drained nor cancelled, leaking one per failed key on a large walk. The stream is now cancelled best-effort before the per-key error is recorded. - bec8e9f: Correct the UploadThing adapter’s
copy()documentation: it claimed the re-upload streams without buffering, butuploadFilesrequires a Blob, so the body is fully buffered in memory — exactly the multi-GB scenario the comment claimed to protect against. The comment now states the real behavior and its memory implications. No behavior change. - f390c80: Fix
usage()miscountingbytesDownfor buffer-backed bodies read viastream(). The wrapper eagerly markedstream()as counted, which only holds for read-once stream sources — buffer-backed files (the memory adapter, or anything a transforming plugin buffered) have a repeatablestream(), so reading one twice double-counted, and opening a stream without reading it zeroed out the count of a latertext()/arrayBuffer()that actually moved the bytes. The count is now claimed by the first read channel that actually moves bytes, at most once per body. - c095770: Fix a nested-key collision in the
versioning()plugin’s version store.a’s version directory (.versions/a/) is a prefix ofa/b’s (.versions/a/b/), soversions("a")reporteda/b’s snapshots as versions ofa,restore("a")could silently overwriteawitha/b’s old bytes, and pruningacould deletea’s only snapshot while countinga/b’s against the limit. Version ids never contain/, so listings now ignore anything deeper than the key’s own directory, andrestore()rejects aversionIdcontaining/. The on-disk layout is unchanged — existing version stores keep working. - 9055fba: Fix
versioning()’s prune reading only the first list page. Once a key’s history exceeded one provider page,items.length <= maxcould be satisfied by a partial page and pruning was skipped or under-counted, so the configuredlimitwasn’t enforced promptly. Prune now paginates the version directory to exhaustion, likeversions()does. - ce0c3f5: Fix an off-by-one in the
zip()plugin’s classic-format limits. The writer accepted exactly 65,535 entries and sizes/offsets of exactly0xFFFFFFFF— but those are the ZIP64 sentinel values, which the plugin’s ownunzip()(and any ZIP64-aware reader) treats as “the real value lives in a ZIP64 record”, so such an archive couldn’t be read back. The limit checks are now>=, refusing the sentinel values themselves.
June 7, 2026
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.
May 31, 2026
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 supportsdryRunto preview the reconciliation plan. Surfaced at parity as the CLIsynccommand and a write-gated MCPsynctool. -
d998ef6: Add directory-style listing to
list: a newdelimiteroption collapses keys into S3-style common prefixes (“folders”), returned inListResult.prefixes. Supported on every adapter with a folder or prefix model — the object stores (S3 family, R2, GCS, Firebase Storage, Azure) andfs/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) advertisesupportsDelimiter: falseand throw rather than silently returning a flat list.The CLI and MCP server expose this too:
files list --delimiter /returns the direct files initemsand the subfolders in aprefixesarray, and the MCPlisttool gains the samedelimiterargument. Both throw on adapters with no folder concept and reject being combined with--all/all(which walks the whole tree). -
0345169: Add read-only
Filesinstances.Pass
readonly: trueto the constructor, or derive a locked view from an existing client withfiles.readonly(), when a caller should be able to read storage but never mutate it:const files = new Files({ adapter: s3({ bucket: "uploads" }), readonly: true, }); const readOnly = files.readonly(); // reuses the same adapter, prefix, timeout, retries, and hooksReads stay available (
download,head,exists,list,listAll,url). Every write surface —upload,delete,copy,move,signedUploadUrl, and the equivalentfile(key)helpers (upload,delete,copyTo,copyFrom,moveTo,moveFrom,signedUploadUrl) — now fails immediately, before the adapter is touched, with a new normalizedFilesError { code: "ReadOnly" }. The failure is deterministic and is not retried;onErrorand the finalonAction({ status: "error" })hooks still fire.The
rawescape hatch is not governed by the guard — code that writes throughfiles.rawbypasses it by design. -
dbf6ded:
uploadnow accepts acontroloption for pause-able and resumable uploads. Construct anUploadControl, pass it in, and pause, resume, or abort the upload — or persistcontrol.toJSON()and resume it later (even in a new process or after a page reload) withUploadControl.from(token).import { Files, UploadControl } from "files-sdk"; const control = new UploadControl(); const promise = files.upload("big.iso", file, { control, multipart: { partSize: 16 * 1024 * 1024 }, onProgress: ({ loaded, total }) => bar.set(loaded, total), }); control.pause(); // in-flight parts settle, the promise stays pending save(control.toJSON()); // serializable session token — persist anywhere control.resume(); // continue // …or, after a crash / reload, in a new process: const result = await files.upload("big.iso", file, { control: UploadControl.from(load()), });
Patch Changes
- 1ff2550: Azure gains a native
deleteManybacked by the Blob Batch API (256 keys per batch, idempotent on already-missing blobs);stopOnErrorfalls 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
maxBytesoverrides 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 inFilesprefixes 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/RNTOand the SFTPRENAMEop) 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-streamstart/endoffsets; FTP begins the transfer at theRESTstart offset and trims a boundedendclient-side. Both adapters now advertisesupportsRange. - e1d09a6: Start the MCP server in read-only mode by default and require
--allow-writesbefore registering mutation tools. - 1ff2550: Gate unsupported
metadata/cacheControlcentrally in theFileswrapper via newAdapter.supportsMetadata/Adapter.supportsCacheControlflags — exactly likesupportsRange. Every adapter is flagged accurately and the per-adapter inline throws (Convex, FTP, SFTP, Dropbox, Box, OneDrive, Cloudinary, Appwrite, PocketBase, Bunny Storage, Bun’s S3) are removed in favor of the one gate. Behavior change: Vercel Blob (metadata), UploadThing (metadata/cacheControl), and SharePoint (metadata/cacheControl) previously dropped these options silently and now throw aFilesError, matching every other adapter. - 1ff2550: R2 (HTTP) now advertises
supportsRange, so ranged downloads work in HTTP mode — it delegates tos3(), which honors theRangerequest. The R2 Workers binding already supported them. - e1d09a6: Reject
responseContentDispositionfor fs, FTP, and SFTP public URLs because those static URLs cannot bind the override into a signature. - e1d09a6: Reject Azure signed upload
contentTypeoverrides because Azure SAS URLs do not bind the request Content-Type into the signature. - e1d09a6: Reject Google Drive, OneDrive, and SharePoint signed upload
maxSizeandminSizeoptions because their upload sessions cannot enforce a server-side content-length policy. - e1d09a6: Reject relative path segments in OneDrive and SharePoint delegated paths before building Microsoft Graph item URLs, keeping
rootFolderPathscoped to its configured folder. - e1d09a6: Scope Google Drive virtual-key file ID resolution to
rootFolderIdby including the configured root folder parent in Drive lookup queries.
May 25, 2026
Minor Changes
-
12d6218: Bring the CLI (and MCP server) to full parity with the SDK surface.
Every
Filescapability is now reachable from thefilesbinary:- Global
--key-prefixscopes every operation under a base path (the instance prefix fromnew Files({ prefix }), distinct from the one-offlist --prefixfilter). Global--timeout/--retriesset the per-attempt timeout and retry count for all commands. download --range start-enddownloads a byte range (0-based, inclusive), e.g.0-1023or1024-.upload --multipart(with--part-size/--multipart-concurrency) uploads large objects in parallel parts.head/exists/deleteaccept--concurrencyand--stop-on-errorto tune the bulk fan-out for many keys.list --allwalks 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), anddownload <keys...> --out-dir <dir>downloads many keys into a directory — both built on the SDK’s bulk array forms.transfercopies every object from the configured (source) provider to another provider given as a JSON config (--to), streaming each body across backends.--prefixfilters the walk and--no-overwriteskips keys already present at the destination.
The MCP server mirrors all of the above: the
uploadtool takesmultipart,downloadtakes a byterange, thehead/exists/deletetools takeconcurrency/stopOnError,listtakesall, and a newtransfertool copies objects across providers. The global--key-prefix/--timeout/--retriesbind to the server’sFilesinstance at startup. - Global
-
0bb7ca3: Add
transferfor cross-provider migration.transfer(source, dest, options?)streams every object from oneFilesinstance to another — the one operation the unified surface uniquely enables, sincecopy/movelive inside a single adapter. It’s built entirely on public primitives (the source’slistAll+ streamingdownload, the destination’sexists+upload), so no adapter implements anything new.import { Files, transfer } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { r2 } from "files-sdk/r2"; const from = new Files({ adapter: s3({ bucket: "old" }) }); const to = new Files({ adapter: r2({ bucket: "new", accountId, accessKeyId, secretAccessKey }), }); const { transferred, skipped, errors } = await transfer(from, to, { prefix: "uploads/", onProgress: ({ done, key }) => console.log(done, key), });Both sides are full
Filesinstances, so each leg honors its ownprefix, retries, timeouts, and hooks. Each object is streamed download-to-upload — the destination never buffers a whole large file. Body, content type, and user metadata travel;etag/lastModifiedare destination-assigned andCache-Controlis not carried.Like the bulk array methods,
transferdoesn’t throw on partial failure: results come back as{ transferred, skipped?, errors? }in walk order. Options coverprefix,transformKey,overwrite(skip keys already present),concurrency(default 8),limit(walk page size),stopOnError(sequential, bail at first failure),signal, andonProgress. -
5d24bc8: Add
hookstonew Files(...)so applications can observe SDK activity withonAction,onError, andonRetry.Each hook is fire-and-forget (called, not awaited) and receives a small, caller-facing event — the operation
type, the publickey/keys(orfrom/toforcopy), timing, and the final result or error. It mirrors the lightweightonProgresscallback style. -
c50a55a: Add an in-memory adapter at
files-sdk/memory. It implements the fullAdaptercontract backed by aMap, so you can test code that usesFileswithout touching disk or real storage — the same swap-in-via-env story as the other adapters, but with nothing to clean up.import { Files } from "files-sdk"; import { memory } from "files-sdk/memory"; const files = new Files({ adapter: memory() }); await files.upload("hello.txt", "hi"); (await files.download("hello.txt")).text(); // "hi"Zero dependencies and isomorphic (no
node:fs/node:crypto), so it runs unchanged in Node, Bun, Deno, the browser, and edge runtimes. Passinitialto pre-populate fixtures, and reach intoadapter.raw(the backingMap) to inspect or reset the store between tests:const adapter = memory({ initial: { "users/1.json": '{"id":1}' } }); adapter.raw.clear();url()returns an opaque, non-fetchablememory://${key}andsignedUploadUrl()amemory://placeholder — there’s no server backing the store. It’s a test/reference adapter, not for production. -
67349f4: Add
moveandlistAll.files.move(from, to, options?)renames a key. It uses the adapter’s native rename where one exists (thefsadapter renames in place atomically; Cloudinary uses its server-siderename, keeping the sameasset_idwith no re-upload) and otherwise falls back tocopy+delete— the same two-step every object store takes, since none offer an atomic move. Moving a key onto itself is a no-op, so the fallback can’t copy-then-delete a file out of existence.movethrows on Convex, wherecopydoes (immutable storage ids, no rename).await files.move("uploads/tmp-abc.png", "avatars/user-123.png");FileHandlegains the matchingmoveTo/moveFrom, andmovefires the lifecycle hooks (onAction/onError/onRetry) with a new"move"action type.files.listAll(options?)walks every page as an async iterable, following the cursor for you:for await (const file of files.listAll({ prefix: "avatars/" })) { console.log(file.key, file.size); }prefixscopes the walk andlimitsets the per-page size; each page is a reallistcall, so retries, timeouts, and prefix scoping all apply.Custom adapters can implement an optional
move(from, to, opts?)to provide a native rename; omitting it keeps the copy + delete fallback.The CLI gains a
move <from> <to>command and the MCP server exposes a matchingmovetool. -
a96874f:
uploadnow accepts amultipartoption for uploading large bodies in parallel parts.await files.upload("backups/db.tar", stream, { multipart: true, // or { partSize, concurrency } });- S3 and the S3-compatible adapters (incl. R2 over HTTP) run multipart through
@aws-sdk/lib-storage, falling back to a singlePutObjectfor small bodies. Unknown-lengthReadableStreambodies now use multipart automatically, even without the flag. - OneDrive uploads above its 250 MB simple-upload limit (and any
multipartrequest) now go through a chunked upload session instead of throwing — large files just work. - GCS and Firebase Storage switch to a resumable upload when
multipartis set;partSizemaps to the chunk size. - Azure Blob maps
partSize/concurrencyto its parallel block-upload tuning. - Dropbox now streams
ReadableStreambodies through its upload session chunk-by-chunk instead of buffering the whole file in memory;partSizetunes the chunk size (rounded to a 4 MiB multiple). - The array form of
uploadaccepts a per-itemmultiparttoggle/tuning too.
Other adapters already stream natively or only accept a fully-buffered body, so they ignore the option.
- S3 and the S3-compatible adapters (incl. R2 over HTTP) run multipart through
-
64cf324:
downloadnow accepts arangeoption for fetching a contiguous byte slice of an object — the primitive behind video seeking and resumable downloads.// Bytes 0–1023 (end is inclusive, matching the HTTP Range header) → 1024 bytes. const head = await files.download("video.mp4", { range: { start: 0, end: 1023 }, }); // Omit end to read from an offset to EOF — e.g. resume an interrupted download. const rest = await files.download("video.mp4", { range: { start: 1024 } });Both bounds are 0-based and
endis inclusive, mirroring thebytes=start-endrequest the supporting adapters issue. The returnedStoredFilecarries just the requested bytes and reports the range length as itssize.rangeworks withas: "stream"so you never buffer the whole slice.- S3 and every S3-compatible adapter (R2 over HTTP, MinIO, DigitalOcean Spaces, Wasabi, Tigris, Backblaze B2, Storj, Hetzner, Akamai, and the rest of the
s3()family) issue a rangedGetObject. - Bun S3 slices via
S3File.slice, GCS and Firebase Storage viacreateReadStream/downloadbyte offsets, Azure Blob via its offset/count download, and the R2 Workers binding via its nativerangeoption. - The local
fsadapter reads only the requested bytes off disk, and the in-memory adapter slices its buffer. - The fetch-based adapters — UploadThing, Box, Vercel Blob (public), Cloudinary, PocketBase, Dropbox, OneDrive, SharePoint, and Google Drive — send an HTTP
Rangeheader and verify the host replied206 Partial Content, throwing if it ignored the range and returned the whole object (so the bandwidth saving is never silently lost).
Adapters whose provider has no range primitive (Supabase, Appwrite, Netlify Blobs, Bunny Storage, Convex, and Vercel Blob private blobs) throw a
FilesErrorrather than downloading the whole object and slicing it client-side. Custom adapters opt in by settingsupportsRange: trueand honoringDownloadOptions.range; theFileswrapper validates the range and gates unsupported adapters before any provider call. - S3 and every S3-compatible adapter (R2 over HTTP, MinIO, DigitalOcean Spaces, Wasabi, Tigris, Backblaze B2, Storj, Hetzner, Akamai, and the rest of the
-
841175a:
uploadnow accepts anonProgresscallback for reporting realtime progress — e.g. to drive a progress bar.await files.upload("big.zip", stream, { onProgress: ({ loaded, total }) => console.log( total ? `${Math.round((loaded / total) * 100)}%` : `${loaded} bytes` ), });Granularity depends on the body and the adapter:
- A
ReadableStreambody is reported byte-by-byte on every adapter, as the bytes are consumed (totalis omitted, since the length is unknown). - A buffered body (
File,Blob,ArrayBuffer,Uint8Array,string) reports{ loaded: 0, total }then{ loaded: total, total }by default. - Adapters with a native upload-progress hook report true byte-level progress for every body type (buffered included): S3 and the S3-compatible adapters, R2 (HTTP), Azure Blob, Google Cloud Storage, Firebase Storage, Vercel Blob, and FTP. The S3 family uses
@aws-sdk/lib-storage(a new optional peer dependency loaded only whenonProgressis used) and also gains multipart for large files; GCS and Firebase Storage switch to a resumable upload whenonProgressis set.
The array form of
uploadacceptsonProgresstoo; each report carries the item’skey. Custom adapters can opt into reporting progress themselves by settingreportsUploadProgress: trueand callingopts.onProgress. - A
Patch Changes
-
52daa66: Update bundled and peer dependencies.
The CLI’s
commanderruntime 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.
-
26989e0: The published package now ships its documentation. The full docs are bundled at
node_modules/files-sdk/docs(per-adapter pages underdocs/adapters/, AI tools underdocs/ai/, plusoverview,api,cli,providers, andtroubleshooting), so tools and agents can read version-matched reference material offline instead of relying on the hosted site. -
7027836: In-memory adapter (
files-sdk/memory): givemetadatathe same value semantics the bytes already have. The adapter cloned an entry’s bytes on the way in but stored and returned themetadataobject by reference, so three aliases leaked: mutating the object passed toupload()(or aninitialseed) after the call reached into the store, mutating ahead()/download()result’smetadatareached back into the store, andcopy()left the source and destination sharing one mutable metadata object — mutating one silently changed the other. Metadata is now shallow-cloned on write and on read, so each stored entry owns its own copy and every read hands back a fresh one, matching how a real backend round-trips metadata. Bytes behavior is unchanged. -
293ba1d:
onProgressis now truly fire-and-forget: a throwing progress reporter can no longer fail or retry the upload it observes. Previously, a buffered upload’s final progress report ran inside the retryable attempt, so a throw was caught by the retry layer, mislabelled a provider error, and re-uploaded the body up toretriestimes before rejecting; on the streaming path a throw errored the underlying stream and failed the upload. All three wrapper-drivenonProgresscalls now route through the same swallow-and-ignore guard thehookscallbacks use, matching the contract already documented onFilesHooks(“a hook that throws can never fail the operation it observes”). Self-reporting adapters (reportsUploadProgress) are unaffected — they own their own reporting. -
1b978b9: Fix
signedUploadUrl({ maxSize })failing with501 Not Implementedon Cloudflare R2.The R2 adapter inherited the S3 adapter’s behaviour of routing
maxSizethrough a presignedPOSTpolicy (content-length-range). Cloudflare R2 does not implement the S3POST ObjectAPI, so those uploads failed at upload time with501 Not Implemented.R2 now throws a clear
Providererror whenmaxSizeis passed (matching how the Azure and Supabase adapters handle the same limitation), instead of handing back a POST form R2 can’t serve. OmitmaxSizeto get a presignedPUTURL, and enforce upload caps at your application gateway. Fixes #49.
May 22, 2026
Minor Changes
-
c6b4df1:
upload,download,head, andexistsnow accept an array for bulk operations, mirroringdelete. 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:const up = await files.upload( [ { key: "avatars/a.png", body: a, contentType: "image/png" }, { key: "avatars/b.png", body: b }, ], { concurrency: 8, stopOnError: false } ); up.uploaded; // UploadResult[] — successes, in the order supplied up.errors; // undefined when every item succeeded const down = await files.download(["a.png", "b.png"]); // { downloaded, errors? } const meta = await files.head(["a.png", "b.png"]); // { files, errors? } const there = await files.exists(["a.png", "b.png"]); // { existing, missing, errors? }upload’s array items are flat — each carries its ownkey,body, and optionalcontentType/cacheControl/metadata. No provider exposes a native batch primitive for these operations, so the SDK always fans out to per-key calls with boundedconcurrency(default 8);stopOnError: false(default) attempts every item and collects per-key failures inerrors, whilestopOnError: truestops at the first failure. All array forms honor the client’sprefixand report the keys the caller passed, not the internal prefixed paths. Invalid keys are reported inerrorsrather than thrown.existssplits results intoexisting/missingand only routes hard errors (auth, transport) toerrors. ThefilesCLI’sheadandexistscommands and the MCPhead/existstools accept multiple keys too. -
ed72daf: Add a Convex storage adapter (
files-sdk/convex). Convex file storage is only reachable from inside a Convex function, so the adapter wraps the function context —convex({ ctx }), constructed per request inside an action, mutation, or query — and maps the unifiedAdaptersurface ontoctx.storage/ctx.db.system. Because Convex assigns the storage id (Id<"_storage">) and exposes no writable metadata, the storage id is the key:upload()returns the assigned id, anddownload/head/delete/urltake it back. Available operations follow Convex’s context rules —upload/downloadneed an action,listneeds a query/mutation — and the adapter throws a descriptive error when a primitive is unavailable.copy, custommetadata, andcacheControlare unsupported;url()returns a permanent serving URL;signedUploadUrl()returns Convex’s raw-body POST upload URL.convexis an optional peer dependency. -
bad4a80:
delete()now accepts an array of keys for bulk deletion. Pass a string to remove one object (resolves tovoid, throws on failure as before); pass an array to remove many in one call and get back a structured{ deleted, errors? }result instead of throwing on partial failure — so you can see exactly which keys failed:const result = await files.delete( ["avatars/a.png", "avatars/b.png", "avatars/c.png"], { concurrency: 8, stopOnError: false } ); result.deleted; // string[] — keys removed, in the order supplied result.errors; // undefined when every key succeededAdapters with a native bulk primitive use it — S3 sends
DeleteObjects(chunked into batches of 1000, the provider limit), Supabase usesremove(keys), and UploadThing usesdeleteFiles(keys)— while every other adapter fans out to single deletes with boundedconcurrency(default 8).stopOnError: false(default) attempts every key and collects per-key failures inerrors;stopOnError: truestops at the first failure. Invalid keys are reported inerrorsrather than thrown, and the array form honors the client’sprefixand is no-op friendly on providers that treat a missing key as success. ThefilesCLI’sdeletecommand and the MCPdeletetool accept multiple keys too. -
9e9fa13: Add FTP and SFTP adapters (
files-sdk/ftp,files-sdk/sftp) for on-prem and legacy file servers. Both expose the standard unified surface, so they’re interchangeable with the cloud adapters:import { Files } from "files-sdk"; import { sftp } from "files-sdk/sftp"; const files = new Files({ adapter: sftp({ host: "files.example.com", username: process.env.SFTP_USERNAME!, privateKey: process.env.SFTP_PRIVATE_KEY!, root: "/uploads", }), }); await files.upload("reports/q1.csv", csv, { contentType: "text/csv" });FTP uses
basic-ftp(with FTPS viasecure: true); SFTP usesssh2-sftp-client. Both are optional peer dependencies. These adapters are Node-only (raw sockets — no edge/browser/Workers support) and connect per operation by default; pass a pre-connectedclientto reuse one connection for batch work. Keys resolve under a configurablerootwith a..traversal guard,listwalks the tree recursively with cursor pagination, anddeleteManyreuses a single connection. These protocols store no MIME type (inferred from the file extension), no arbitrarymetadata/cacheControl(both throw), and serve no HTTP —url()requires apublicBaseUrlpointing at an HTTP server fronting the same tree, andsignedUploadUrl()throws.copyround-trips the bytes through the client since neither protocol has a portable server-side copy. -
1eb1dfc: Add a
files-sdk/providersexport: a zero-dependency catalog of every storage provider and the environment variables each one reads.PROVIDERSmaps each slug to its display name, description, optional peer dependencies, and a structured env spec —requiredvars, mutually exclusivecredentialModes(so Azure’s connection-string-or-key-or-SAS choice is expressible),optionaltuning vars, and non-envconfig. Every variable is taggedsecretandreadBy("files-sdk"vs the underlying SDK’s"sdk-chain", so AWS/GCS credential-chain vars aren’t mislabeled as required). Helpers:getProvider,listEnvVars,getSecretEnvVars.PROVIDER_NAMESand theProvider/ProviderSlugtypes are also re-exported from the package root. Useful for sync engines, config UIs, and onboarding flows that need to enumerate providers and their required configuration up front.
Patch Changes
-
e80e922: Add
signal,timeout, andretriesto every operation. Set them on theFilesconstructor as defaults and override per call (a per-call value wins).retriesis a number or{ max, backoff }; onlyProviderfailures are retried —NotFound,Unauthorized,Conflict, aborts, and timeouts are returned immediately, andReadableStreamuploads 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 ownbackoff({ attempt, error })for jitter or a different curve.timeoutis applied per attempt and aborts the operation rather than triggering a retry. Asignalalways fails fast at theFileslayer 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 (downloadandlist— 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 theFileslayer 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 forurl(),signedUploadUrl(), and same-containercopy(), so signed URLs keep working without a storage account key. SetuseUserDelegationSas: falseto opt out of SAS signing for token-only setups. -
dbda237: Add a
prefixoption to theFilesconstructor. 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 (andname) returned in results, so your application code works in its own namespace:const users = new Files({ adapter: s3({ bucket: "uploads" }), prefix: "users", }); await users.upload("123/avatar.png", file); // writes users/123/avatar.png const stored = await users.head("123/avatar.png"); stored.key; // "123/avatar.png" - prefix strippedLeading and trailing slashes on the prefix are normalized (
"/users/"and"users"behave identically), andlist()scopes the underlying query on a path boundary so aprefix: "users"instance never matches the siblingusers-archive/. -
d921741: Harden three internal regexes against polynomial ReDoS. The trailing-slash/
[. ]-stripping patterns innormalizePrefix(core, used by every adapter’s prefix handling), thefsadapter’s Windows trailing-noise check, and thebunny-storagekey parser each anchor with a(?<!…)lookbehind so the engine can’t re-attempt the match at every character of a long trailing run (e.g."users////…"or"x.meta.json.... "). Behavior is unchanged; only the worst-case matching cost is fixed. -
ff39a2e: fs adapter: reject keys that resolve to a
.meta.jsonsidecar path — the adapter reserves that suffix for its per-object metadata sidecar, and accepting it as a regular key let a same-root caller silently overwrite, hide, or delete another key’s sidecar (flipping the servedContent-Type, mutating arbitrarymetadatafields, or stripping the etag). The check runs on the resolved basename and folds case plus Windows trailing dots/spaces, so re-cased or normalized variants (x.META.JSON,x.meta.json.,x.meta.json/) that alias the sidecar on case-insensitive (APFS/NTFS) or Windows volumes are rejected too. -
979cd00: Add Vercel OIDC authentication to the Vercel Blob adapter (
files-sdk/vercel-blob). When the Blob store is connected to a Vercel project, the adapter now automatically picks upVERCEL_OIDC_TOKEN+BLOB_STORE_IDand uses OIDC instead of the long-livedBLOB_READ_WRITE_TOKEN— OIDC tokens rotate automatically, which removes the risk that a static secret leaks from your codebase or environment. Two new options,oidcTokenandstoreId, let you pass OIDC credentials explicitly for runtimes (e.g. Vite) that don’t load.env.localintoprocess.env. Credential resolution mirrors the upstream SDK exactly: an explicittokenalways wins, then OIDC (option or env), thenBLOB_READ_WRITE_TOKEN. Theurl()fast path now usesstoreId(option orBLOB_STORE_IDenv) when present so OIDC users keep the no-round-trip behavior, andBLOB_STORE_IDis accepted in eitherstore_<id>or<id>form. Bumps the@vercel/blobpeer dep floor to^2.4.0, which is the first version that ships the OIDC options.
May 18, 2026
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 fromALIBABA_ACCESS_KEY_IDandALIBABA_ACCESS_KEY_SECRET. -
d619709: Add
filesCLI for agents and scripts. One binary covers every adapter via--provider <name>with lazy imports — cold-start cost matches whichever single provider you select. EachAdaptermethod maps to a subcommand (upload,download,head,exists,delete,copy,list,url,sign-upload), with JSON-by-default output,stdin/stdoutstreaming for binary bodies,--dry-runand--verbosemodes, 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-jsonis an escape hatch for the long tail of adapter options.files ... mcpboots 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 toresource_type: "raw"for arbitrary-bytes storage; switch toimage/videofor transforms. ReadsCLOUDINARY_URLor individualCLOUDINARY_*env vars. Full Adapter surface including signed delivery URLs forprivate/authenticatedtypes and form-POST signed upload URLs. -
8b62142: Add Firebase Storage adapter (
files-sdk/firebase-storage). Wraps the officialfirebase-adminSDK; the underlyinggetStorage().bucket()returns a@google-cloud/storageBucket, so V4 signed read URLs, POST policy uploads withmaxSize, server-side copy, and the full metadata round-trip all work out of the box. Auto-loads credentials fromFIREBASE_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 existingApporBucketviaappto share initialization with Firestore/Auth. The bucket name defaults to<projectId>.firebasestorage.appwhen neitherbucketnorFIREBASE_STORAGE_BUCKETis set. Firebase’s?alt=media&token=…download-token URL form is out of scope for v1 — reach foradapter.rawif you need it. -
8b62142: Add PocketBase adapter (
files-sdk/pocketbase). Wraps the officialpocketbaseJS SDK and maps the unified key/blob API onto a dedicated collection: each upload becomes (or updates) a record whose configurablekeyField(unique-indexed text, default"key") holds the user-facing key and whose configurablefileField(single-value file, default"file") holds the body. Auto-loads fromPOCKETBASE_URLplus eitherPOCKETBASE_ADMIN_EMAIL+POCKETBASE_ADMIN_PASSWORD(admin login on first call) orPOCKETBASE_AUTH_TOKEN(pre-issued token); accepts an existingPocketBaseclient viaclient.url()returnspb.files.getURL(), threading a short-lived file token frompb.files.getToken()for authenticated clients; setpublicBaseUrlfor 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.UploadOptionscacheControlandmetadatathrow — 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 viarawif you need them.responseContentDispositiononurl()throws — userawand the?download=truequery string instead. -
d0aec82: Add SharePoint adapter (
files-sdk/sharepoint). ResolvessiteUrland nameddocumentLibraryto a drive via Microsoft Graph, then delegates to the OneDrive adapter for file operations. Falls back toSHAREPOINT_*env vars then toONEDRIVE_*. 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 fromTENCENT_SECRET_IDandTENCENT_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 toru-central1for signing, virtual-hosted-style addressing, errors relabelled as “Yandex Cloud error”. Auto-loads fromYANDEX_ACCESS_KEY_IDandYANDEX_SECRET_ACCESS_KEY. -
de63748: Add Bun S3 adapter at
files-sdk/bun-s3, backed by Bun’s nativeBun.S3Clientinstead of@aws-sdk/client-s3. Use this when you’re already on Bun and want to skip the AWS SDK dependency. Implements the full adapter surface (upload, download, head, exists, delete, copy, list, url, signedUploadUrl) with three deliberate limitations vsfiles-sdk/s3:copy()is client-side (Bun has no server-sideCopyObjectprimitive), andupload(metadata|cacheControl)plussignedUploadUrl(maxSize)throw becauseBun.S3Clientdoesn’t expose equivalent options. Passclient: Bun.s3to reuse the global singleton, or hand in any customBun.S3Client-shaped instance. -
28e3243: Add Bunny Storage adapter (
files-sdk/bunny-storage). Wraps the official@bunny.net/storage-sdkand connects to a Storage Zone via zone name + access key + region. Auto-loads fromBUNNY_STORAGE_ZONE/BUNNY_STORAGE_ACCESS_KEY/BUNNY_STORAGE_REGION, withSTORAGE_*accepted as aliases (the names used in the Bunny SDK’s README).url()requirespublicBaseUrl(typically a Bunny Pull Zone) and returns a permanent CDN URL — Bunny has no signed-read primitive, soexpiresInis ignored andresponseContentDispositionthrows.signedUploadUrl()throws because Bunny writes require the Storage APIAccessKeyheader.copy()is a read-then-write (no server-side copy primitive in the SDK). CustommetadataandcacheControlon upload throw — configure cache behavior on the Pull Zone instead. -
78bcf37: Move provider SDKs to optional peer dependencies. Installing
files-sdkno longer pulls in every provider SDK by default — the package fully installs at a fraction of the previous size, and unused providers can’t drag in transitive CVEs. Install only what you use:# S3 (and any S3-compatible: R2, MinIO, DigitalOcean Spaces, …) npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner # GCS npm install files-sdk @google-cloud/storage google-auth-library # Azure npm install files-sdk @azure/storage-blob @azure/identityBreaking (install-time only): if you upgrade and your project doesn’t list the relevant provider SDK in its own
package.json, the next adapter import will throwERR_MODULE_NOT_FOUND. Fix is onenpm install. The published JS for each adapter subpath (files-sdk/s3,files-sdk/gcs, …) is byte-identical to the previous release — provider SDKs were already externalized, so runtime behavior, tree-shaking, and bundle sizes don’t change. ThefilesCLI keepscommanderas a regular dep, sonpx filesworks out of the box. Fixes #34.
Patch Changes
- a53be2d: Expand adapter test coverage for error-recovery branches that were previously unexercised:
exists()swallowing a thrownNotFound(azure, gcs, netlify-blobs, r2) versus rethrowing other mapped errors; the supabase stream-download error envelope; and dropbox’sexists()returning false forfolder/deleted.tags plus theshared_link_already_existsrecovery falling through when no usable URL is embedded. No runtime behavior changes.
May 15, 2026
Minor Changes
- 2d3a569: Add Appwrite adapter at
files-sdk/appwriteexportingappwrite(), a wrapper around the officialnode-appwriteSDK’sStorageAPI. Auto-loadsendpoint,projectId, andkeyfromAPPWRITE_ENDPOINT/APPWRITE_PROJECT_ID/APPWRITE_API_KEY(withNEXT_PUBLIC_*fallbacks for the first two), or accepts an existingClientorStorageinstance viaclient.list({ prefix })is forwarded as astartsWith("$id", prefix)query against the canonical file ID — files created outside the adapter where the displaynamediffers from$idwon’t be matched by prefix.upload()buffers stream bodies up-front sinceInputFile.fromBufferhas no streaming form, throws onUploadOptions.cacheControland non-emptyUploadOptions.metadata(Appwrite has no equivalent fields), and silently ignoresUploadOptions.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); setpublic: trueon a public bucket to return the constructed permanentviewURL.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 aFilesError("Provider", ...)before the API call. Errors are relabelled asAppwrite error, with404/401+403/409mapped toNotFound/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 fromB2_APPLICATION_KEY_ID/B2_APPLICATION_KEY. Errors are relabelled asBackblaze B2 errorandpublicBaseUrlaccepts B2’s friendly download URL prefix for skipping signing on public buckets. - 2a35ce1: Add
exists(key)to the Files API. Returnstruewhen the object exists andfalsewhen 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 fromEXOSCALE_API_KEY/EXOSCALE_API_SECRET. Exoscale calls these zones but they fill the SigV4 region slot. Errors are relabelled asExoscale error. - 2c52f56: Add
files.file(key)to return aFileHandlebound to a single key. The handle exposesupload,download,head,exists,delete,url,signedUploadUrl,copyTo, andcopyFromwithout re-passing the key each time. It’s a thin wrapper over the sameFilesmethods, 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 fixedhttps://s3.filebase.comendpoint with virtual-hosted-style addressing, defaults the SigV4 region to"us-east-1", and auto-loads credentials fromFILEBASE_ACCESS_KEY_ID/FILEBASE_SECRET_ACCESS_KEY.publicBaseUrlaccepts an IPFS/Sia/Storj gateway prefix for skipping signing on public objects. Errors are relabelled asFilebase 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 fromIBM_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, passhttps://s3.direct.<region>.cloud-object-storage.appdomain.cloudas an explicitendpoint. Errors are relabelled asIBM Cloud Object Storage error. - 8ae51f0: Add iDrive e2 adapter at
files-sdk/idrive-e2, a thin S3 wrapper that takes an explicitendpoint(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 fromIDRIVE_E2_ACCESS_KEY_ID/IDRIVE_E2_SECRET_ACCESS_KEY. Errors are relabelled asiDrive e2 error. - 8ae51f0: Add Oracle Cloud Infrastructure Object Storage adapter at
files-sdk/oracle-cloud, a thin S3 wrapper around OCI’s S3 compatibility layer. Requires both the tenancynamespaceand aregionto derive the endpoint (<namespace>.compat.objectstorage.<region>.oraclecloud.com); defaults to path-style addressing since OCI’s wildcard TLS cert doesn’t cover bucket subdomains under the namespace-prefixed host. Auth uses OCI’s HMAC Customer Secret Keys (distinct from regular API signing keys); credentials auto-load fromOCI_ACCESS_KEY_ID/OCI_SECRET_ACCESS_KEY. Errors are relabelled asOracle Cloud error. - 8ae51f0: Add OVHcloud Object Storage adapter at
files-sdk/ovhcloud, a thin S3 wrapper that derives the endpoint from the region code (s3.<region>.io.cloud.ovh.net— High Performance S3 tier), defaults to virtual-hosted-style addressing, and auto-loads credentials fromOVH_ACCESS_KEY_ID/OVH_SECRET_ACCESS_KEY. For the Standard (Swift-backed) tier, passhttps://s3.<region>.cloud.ovh.netas an explicitendpoint. Errors are relabelled asOVHcloud error. - 8ae51f0: Add Scaleway Object Storage adapter at
files-sdk/scaleway, a thin S3 wrapper that derives the endpoint from the region code (s3.<region>.scw.cloud—fr-par,nl-ams,pl-waw), defaults to virtual-hosted-style addressing, and auto-loads credentials fromSCW_ACCESS_KEY/SCW_SECRET_KEY. Errors are relabelled asScaleway error. - ed87e51: Add Tigris adapter at
files-sdk/tigris, a thin S3 wrapper around Tigris’s globally-distributed object storage. Uses the fixedhttps://fly.storage.tigris.devendpoint with virtual-hosted-style addressing, defaults the SigV4 region to"auto"since Tigris doesn’t route by region, and auto-loads credentials fromTIGRIS_ACCESS_KEY_ID/TIGRIS_SECRET_ACCESS_KEY. Errors are relabelled asTigris error. - 8ae51f0: Add Vultr Object Storage adapter at
files-sdk/vultr, a thin S3 wrapper that derives the endpoint from the region code (<region>.vultrobjects.com—ewr,sjc,ams,blr,del,sgp,lux), defaults to virtual-hosted-style addressing, and auto-loads credentials fromVULTR_ACCESS_KEY_ID/VULTR_SECRET_ACCESS_KEY. Errors are relabelled asVultr error. - ed87e51: Add Wasabi adapter at
files-sdk/wasabi, a thin S3 wrapper that derives the endpoint from the region code (s3.<region>.wasabisys.com), defaults to virtual-hosted-style addressing, and auto-loads credentials fromWASABI_ACCESS_KEY_ID/WASABI_SECRET_ACCESS_KEY. Region names mirror AWS but the endpoints are Wasabi’s own; errors are relabelled asWasabi error.
Patch Changes
-
2aa92e1: URL-encode keys in
joinPublicUrlto 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.
%20becomes%2520). -
8982c51: Expand test coverage for
box,fs,onedrive,supabase, andopenai/responsesadapters. Adds tests coveringmapBoxError/mapGraphErrornon-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.
May 10, 2026
Minor Changes
-
9758347: Add AI SDK tools subpath (
files-sdk/ai-sdk) exportingcreateFileTools(...)— wraps a configuredFilesinstance as a set of Vercel AI SDK tools (listFiles,getFileMetadata,downloadFile,getFileUrl,uploadFile,deleteFile,copyFile,signUploadUrl) ready to plug intogenerateText/streamText/ any agent. Mirrors@github-tools/sdk’s ergonomics: write tools require approval by default (configurable globally or per-tool viarequireApproval),readOnly: truestrips writes entirely, andoverrideslets callers patch tool descriptions/titles/etc. without touchingexecute. Individual tool factories (uploadFile,downloadFile, …) are also exported for cherry-picking.aiandzodare optional peer dependencies — only required when consuming the new subpath. -
2d811b1: Add Claude Agent SDK tools subpath (
files-sdk/claude) exportingcreateClaudeFileTools(...)— wraps a configuredFilesinstance as an in-process MCP server ready to drop intoquery()from@anthropic-ai/claude-agent-sdk(the renamed Claude Code SDK).The Claude Agent SDK consumes tools differently than the OpenAI/Vercel adapters: tools are bundled into an
SdkMcpServerand surfaced to the agent viamcpServers+allowedTools, with approval enforced through a top-levelcanUseToolcallback. The factory returns all four pieces:const tools = createClaudeFileTools({ files }); for await (const msg of query({ prompt: "List my files.", options: { mcpServers: tools.mcpServers, allowedTools: tools.allowedTools, canUseTool: tools.canUseTool, }, })) { /* ... */ }Same eight file operations as the other AI subpaths (
listFiles,getFileMetadata,downloadFile,getFileUrl,uploadFile,deleteFile,copyFile,signUploadUrl) with the same approval-gating defaults,readOnlymode, and per-tooloverrides(description + MCPannotations). The bundledcanUseTooldenies approval-gated writes; compose your own usingtools.needsApproval(name)for human-in-the-loop UX — it accepts both bare names ("uploadFile") and the MCP-prefixed form ("mcp__files__uploadFile") the SDK passes in. The MCP server name defaults to"files"and is configurable viaserverName, which also flows through to themcp__<server>__*strings inallowedTools. Read tools get areadOnlyHintannotation; writes getdestructiveHint(copyFile/signUploadUrluseidempotentHintinstead).Individual tool factories (
claudeUploadFile,claudeDownloadFile, …) are also exported asSdkMcpToolDefinitioninstances for callers that want to compose their owncreateSdkMcpServerrather than use the bundled one.@anthropic-ai/claude-agent-sdkandzodare optional peer dependencies — only required when consuming the new subpath. -
d6adeae: Add OpenAI tools subpath (
files-sdk/openai) with two factories:createResponsesFileTools(...)— for OpenAI’s native Responses API. Returns{ definitions, execute, needsApproval }.definitionsis the array of function-tool specs to pass intoopenai.responses.create({ tools }).execute(call)runs afunction_callitem and returns afunction_call_outputready to push into the next turn’s input — JSON parse failures and Zod validation errors come back as the tool’s output so the model can self-correct.createAgentsFileTools(...)— for the OpenAI Agents SDK (@openai/agents). Returns a record oftool()outputs ready to spread intonew Agent({ tools }).
Both wrap the same eight file operations as
files-sdk/ai-sdk(listFiles,getFileMetadata,downloadFile,getFileUrl,uploadFile,deleteFile,copyFile,signUploadUrl) with the same approval-gating defaults,readOnlymode, and per-tool overrides. Schemas + execute logic are extracted to a shared internal module so the three subpaths can’t drift apart.openaiand@openai/agentsare optional peer dependencies — install only the one(s) you use. The subpath requires Zod 4.
May 10, 2026
Patch Changes
- 6edb433:
googleDriveandonedriveadapters now auto-load credentials fromprocess.envwhen not passed explicitly, matching the convention already in place for the other adapters.googleDrive()readsGOOGLE_DRIVE_CLIENT_EMAIL+GOOGLE_DRIVE_PRIVATE_KEY(service-account credentials) orGOOGLE_DRIVE_KEY_FILE(path to a service-account JSON), plusGOOGLE_DRIVE_SUBJECTfor domain-wide delegation,GOOGLE_DRIVE_IDto target a Shared Drive, andGOOGLE_DRIVE_ROOT_FOLDER_IDto override the bucket root (when onlyGOOGLE_DRIVE_IDis set,rootFolderIddefaults to the drive id so Shared Drives work with no extra config).onedrive()readsONEDRIVE_ACCESS_TOKEN(static token) or theONEDRIVE_TENANT_ID+ONEDRIVE_CLIENT_ID+ONEDRIVE_CLIENT_SECRETtriple (client-credentials/app-only auth), plusONEDRIVE_DRIVE_ID/ONEDRIVE_SITE_ID/ONEDRIVE_USER_IDto 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.
May 9, 2026
Patch Changes
- bd31113: Fix release workflow referencing a non-existent
VERCEL_PROJECT_ID_WEBsecret; now readsVERCEL_PROJECT_IDto match the configured repository secret so the post-publish Vercel deploy succeeds.
May 9, 2026
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 theregioncluster code (us-iad-1,nl-ams-1,fr-par-1, the olderus-east-1/eu-central-1/ap-south-1clusters, etc.) ashttps://<region>.linodeobjects.comand overridable, virtual-hosted-style addressing,"Akamai error"provider label, andAKAMAI_ACCESS_KEY_ID/AKAMAI_SECRET_ACCESS_KEYenv-var fallbacks.publicBaseUrlaccepts a public-bucket origin (https://<bucket>.<region>.linodeobjects.com) or a custom CNAME for unsigned URLs; otherwiseurl()returns a presigned GetObject (1-hour default). - f40e0d3: Add Box adapter (
files-sdk/box) for personal Box and Box Enterprise via the officialbox-typescript-sdk-genSDK. Box files live by ID rather than by path, so the adapter walksrootFolderIdand translates virtual keys (docs/a.txt) into nested Box subfolders, auto-creating intermediate folders onupload()and racing-recovering onitem_name_in_use. Five auth shapes (pre-builtclient,developerToken,oauthwith refresh-token seeding,ccgwithenterpriseIdoruserId, andjwtwithconfigJsonStringorconfigFilePath) cover scripts, user apps, and enterprise installs; env-var fallback viaBOX_DEVELOPER_TOKEN. Token lifecycle is handled by the SDK’s built-inAuthenticationclasses — no manual refresh bookkeeping. Directupload()uses single-calluploads.uploadFileup to 50 MB and switches tochunkedUploads.uploadBigFileautomatically; existing leaf names route throughuploadFileVersion(overwrite).url()mints a signed download URL viagetDownloadFileUrlby default; withpublicByDefault: true,upload()also callsaddShareLinkToFile(open access) andurl()returns the link’sdownload_url;responseContentDispositionalways throws (no override on Box URLs).signedUploadUrl()throws — Box uploads require a multipart POST with both anattributesJSON part and the file bytes part, which fits neither the SDK’s PUT-with-headers nor POST-with-form-fields shape; useupload()server-side or Box’s UI Elements / Content Uploader for browser flows.list()returns immediate-children files only atrootFolderId(no recursion, subfolders filtered out, prefix matched client-side, offset encoded as a numeric cursor). UsermetadataandcacheControlthrow (Box exposes file metadata via classifications and metadata templates — drop toraw.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 fromregion(https://${region}.digitaloceanspaces.com), virtual-hosted addressing,"Spaces error"provider label, andDO_SPACES_KEY/DO_SPACES_SECRETenv-var fallbacks.publicBaseUrlaccepts 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 officialdropboxSDK. Path-addressable like OneDrive, so virtual keys map directly to Dropbox paths — no virtual-key cache. Four auth shapes (pre-builtclient, static or callableaccessToken, OAuth refresh-token flow withrefreshToken+appKey(+ optionalappSecret), and env-var fallback viaDROPBOX_ACCESS_TOKENorDROPBOX_REFRESH_TOKEN+DROPBOX_APP_KEY(+DROPBOX_APP_SECRET)). Refresh tokens are exchanged atapi.dropboxapi.com/oauth2/tokenand cached until ~60s before expiry.url()mints a 4-hour temporary link viafilesGetTemporaryLinkby default; withpublicByDefault: true,upload()also creates a public shared link andurl()returns it (rewritten to?dl=1for direct download);expiresInis capped at Dropbox’s 14400s (4h) maximum andresponseContentDispositionalways 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; useupload()or drop toraw.filesGetTemporaryUploadLink(...). Directupload()uses single-callfilesUploadup to 150 MB and switches tofilesUploadSession*(chunked, up to 350 GB) automatically; usermetadataandcacheControlthrow (Dropbox files have no native arbitrary-metadata field — userawwithproperty_groupsif you need it). - 5ff9d79: Add Google Drive adapter (
files-sdk/google-drive) via the official@googleapis/drivev3 client. Drive has no native key field, so the adapter maps virtual keys ontoappProperties.fsdkKeyand amortizes lookups with a per-instance LRU cache (configurable viafileIdCacheSize, defaults to 1024). Three auth shapes: inline service-accountcredentials, akeyFilenameJSON path, or 3-leggedoauthrefresh tokens — plus a pre-builtclientescape hatch (note:signedUploadUrl()requires an auth handle and throws when constructed viaclient).signedUploadUrl()initiates a Drive resumable session and returns the session URL as a one-shot PUT (maxSizeis forwarded asX-Upload-Content-Lengthadvisory only;minSizeis ignored).url()requirespublicByDefault: true(grantsanyone, readeron upload and returns the permanent Drive download URL);expiresInignored,responseContentDispositionalways throws. Service-account workloads should target a Shared Drive viadriveIdto avoid the 15 GB personal quota. Callermetadatakeys starting withfsdkare reserved. - 2a84ef2: Add Hetzner Object Storage adapter (
files-sdk/hetzner). Thin wrapper over the S3 adapter with Hetzner defaults: endpoint derived from theregionlocation code (fsn1,nbg1,hel1) ashttps://<region>.your-objectstorage.comand overridable, virtual-hosted-style addressing,"Hetzner error"provider label, andHCLOUD_ACCESS_KEY_ID/HCLOUD_SECRET_ACCESS_KEYenv-var fallbacks.publicBaseUrlaccepts a custom CNAME or proxy host for unsigned URLs; otherwiseurl()returns a presigned GetObject (1-hour default). - b4fd387: Add Netlify Blobs adapter (
files-sdk/netlify-blobs). Wraps the@netlify/blobsSDK with site-scoped or deploy-scoped stores, configurable consistency, and a metadata round-trip that packscontentType/size/lastModified/cacheControlplus user metadata into Netlify’s metadata map sohead()/download()return rich fields. Auto-detects credentials from Netlify’s runtime context (NETLIFY_BLOBS_CONTEXT) when available, with explicitsiteID/tokenoverrides falling back toNETLIFY_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-itemhead());url()andsignedUploadUrl()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 (clientCredentialsfor app-only,oauthfor delegated refresh-token flow,accessTokenfor caller-managed tokens, and a pre-builtclientescape hatch) and four drive targets (/me/drive,driveId,siteId,userId).signedUploadUrl()returns a Graph upload-session URL (one-shot PUT, advisorymaxSize/minSize);url()requirespublicByDefault: trueand creates an anonymous-view share link (Graph has no signed URL primitive,expiresInignored).copy()polls Graph’s async copy monitor with a configurablecopyTimeoutMs. Directupload()is capped at OneDrive’s 250 MB simple-upload limit; usermetadataandcacheControlthrow (Graph drive items have no native arbitrary-metadata field — userawfor Open Extensions). - 7251d42: Add Storj adapter (
files-sdk/storj). Thin wrapper over the S3 adapter with Storj defaults:endpointdefaults tohttps://gateway.storjshare.io(Gateway MT, the hosted multi-tenant gateway) and is overridable for self-hosted Gateway ST, path-style addressing on, region defaulted tous-east-1(the gateway ignores it for routing),"Storj error"provider label, andSTORJ_ACCESS_KEY_ID/STORJ_SECRET_ACCESS_KEYenv-var fallbacks.publicBaseUrlaccepts a linksharing prefix likehttps://link.storjshare.io/raw/<accessGrant>/<bucket>for unsigned URLs. - 37be6fc: Add UploadThing adapter (
files-sdk/uploadthing). Maps the user-supplied key onto UploadThing’scustomId, supports public-read and private ACLs, signs UFS presigned PUT URLs via Web Crypto HMAC-SHA256, and falls back to HEAD-on-URL forhead()and read-then-write forcopy()since UploadThing has no native primitives for those.
Patch Changes
-
0ec97d0: Extract shared adapter helpers into
src/internal/core.tsso authoring a new adapter is less boilerplate. The new module exportsDEFAULT_URL_EXPIRES_IN,joinPublicUrl,resolveUrlStrategy(the two-state public-vs-sign decision, withresponseContentDispositionalways forcing signing),normalizeBody(Body →Uint8Array | ReadableStream<Uint8Array>+ content-type/length), andmakeErrorMapper(factory for the per-providermapXErrorscaffold — code-set lookup, HTTP-status fallback,FilesErrorpass-through). The s3, azure, gcs, supabase, r2, fs, and uploadthing adapters now consume these helpers; supabase keeps its ownnormalizeBodybecause Blob pass-through is required for multipart uploads, and r2’surl()keeps its three-state hybrid logic.mapS3Errorretains 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
rawgetter’s pre/post-init behavior) and for fs uploads withArrayBufferandArrayBufferViewbodies plus rejection of keys that resolve to the adapter root. Drops the unreachableReadableStreambranch infs/bodyToBytes— stream uploads route throughwriteStreamToTempThenRename, so the parameter type is narrowed toExclude<Body, ReadableStream<Uint8Array>>to enforce that at the type level.Further hardens coverage of edge paths across the fs, azure, supabase, r2, and stored-file modules: corrupt/partial sidecar JSON handling, lazy-body errors when an underlying file is removed, atomic upload cleanup when rename fails (buffer + stream paths), non-ENOENT delete errors, Azure stream downloads with missing
readableStreamBody, anonymous Azure copy source URLs, Supabase numericstatusCodefallback and Date/numberlastModifiedparsing, R2 binding copy with put failure, and concurrent reads on a lazyStoredFilesharing the in-flight cache promise.
May 9, 2026
Major Changes
- 30900e6: Initial release