API
The plugin contract types and helpers - FilesPlugin, FilesOperation, handlers, and createFiles. See the overview for how they compose.
The types and helpers that make up the plugin system. See the overview for how wrap, extend, and the pipeline fit together.
FilesPlugin
The plugin object you pass to plugins. A name plus three optional hooks - wrap and extend, which the overview covers, and capabilities, which narrows what the instance advertises.
namestring
Identifies the plugin in collision errors and diagnostics.
stringwrap?<O extends FilesOperation>(
op: O,
next: PluginNext
) => Promise<OperationResult<O>>
Tier A/B: wrap any operation. Call `next` to continue inward.
<O extends FilesOperation>(
op: O,
next: PluginNext
) => Promise<OperationResult<O>>extend?(files: Files) => Ext
Tier C: contribute namespaced surface. The only part that changes the type.
(files: Files) => Extcapabilities?(caps: AdapterCapabilities) => AdapterCapabilities
Narrow what the instance advertises through {@link Files.capabilities}. Receives the snapshot so far — the adapter-derived flags, already folded through every earlier plugin's hook in `plugins` order — and returns the snapshot to advertise. A body-transforming plugin uses it to turn off what it can't honor end to end: `signedUrl.supported` (a presigned URL would serve the stored, transformed bytes) and `rangeRead` (a byte range of the stored bytes isn't a range of the caller's), so gateways and callers that branch on capabilities pick the path that works instead of hitting a fail-closed throw. Return a new object rather than mutating the argument. Only narrow — the hook changes what is advertised, not what the adapter can do, so widening a flag the core gates on doesn't make that option work. Carried into {@link Files.readonly} clones with the rest of the plugin.
(caps: AdapterCapabilities) => AdapterCapabilitiescapabilities
capabilities?: (caps: AdapterCapabilities) => AdapterCapabilities;
Narrows what the instance reports through files.capabilities. The getter starts from the adapter’s snapshot and folds every installed plugin’s capabilities hook over it in plugins order, so each hook receives the snapshot as the earlier plugins left it. files.readonly() clones carry the plugins, so they report the same result.
Use it to advertise what your plugin refuses, so callers and the UI gateway (which picks redirect vs. proxy from capabilities) choose a path that works instead of hitting a fail-closed throw. The bundled body-transforming plugins do this: encryption() and compression() report signedUrl.supported and rangeRead as false (a presigned URL or a byte range would serve the stored, transformed bytes) along with multipart (they refuse resumable uploads), and dedup() reports signedUrl.supported and every conditional flag as false. The plugins that veto conditional operations clear exactly the flags they refuse: tiering() and failover() every conditional flag, versioning() all but exactRead, and softDelete() conditional.delete.
import { handlers, type FilesPlugin } from "files-sdk";
const watermark: FilesPlugin = {
name: "watermark",
// A presigned URL would skip the watermark, so don't advertise one.
capabilities: (caps) => ({ ...caps, signedUrl: { supported: false } }),
wrap: handlers({
download: async (op, next) => addWatermark(await next(op)),
}),
};
Return a new object rather than mutating the argument, and only narrow. The hook changes what is advertised, not what the adapter can do, so turning on a flag the core gates on doesn’t make that option work.
FilesOperation
The discriminated union handed to wrap - one family per public verb, carrying the caller-facing inputs. Native conditional operations stay in the existing upload, download, delete, and copy families and add a mode plus their ETag predicate. The array forms of upload / download / head / exists / delete set bulk: true on each fanned-out item; conditional modes are single-key only.
kind"upload"
"upload"options?AdapterUploadOptions
AdapterUploadOptionshandlers
function handlers(map: PluginHandlers): NonNullable<FilesPlugin["wrap"]>;
Builds a wrap from a per-verb map. Each handler is typed to its own operation and a same-kind next; verbs absent from the map pass through untouched.
An upload handler therefore sees ordinary uploads (no mode) as well as conditional creates and replaces (mode: "create" | "replace"); the same holds for download ("exact"), delete ("match"), and copy ("conditional"). Ordinary operations carry no mode at all, so branch on isConditionalOperation(op) or on the conditional literals, never on an “overwrite” value. Existing body-transforming handlers cover conditional writes automatically. The engine preserves a conditional root’s family, mode, and predicate across every next() call and requires exactly one native operation before success.
rejectConditional
function rejectConditional(
op: Pick<ConditionalFilesOperation, "kind">,
plugin: string,
reason: string
): never;
Veto a conditional operation from inside wrap, before any provider I/O, by throwing a permanent Provider FilesError worded <plugin>: conditional <kind> is unsupported because <reason>. Use it for the modes your plugin cannot make atomic because of a side effect of its own — a snapshot, a mirror write, a pointer rewrite — that would sit outside the native compare-and-set. Every bundled plugin that vetoes uses this shape.
import { isConditionalOperation, rejectConditional } from "files-sdk";
const mirror: FilesPlugin = {
name: "mirror",
wrap: (op, next) => {
if (isConditionalOperation(op)) {
rejectConditional(
op,
"mirror",
"the mirror write cannot share one native compare-and-set"
);
}
return next(op);
},
};
createFiles
function createFiles<A extends Adapter, const P extends readonly FilesPlugin[]>(
opts: FilesOptions<A> & { plugins?: P }
): Files<A> & ExtensionsOf<P>;
Constructs a Files instance whose type includes every plugin’s extend surface. Runtime-identical to new Files(opts) - it exists only to surface the added methods on the type.