validation
A fail-closed guard that vets every upload before any bytes move - enforce a max/min size, an allowed-MIME-type list, and a key-naming rule. Provider-agnostic, no native dependencies, no metadata.
The built-in validation() plugin is a fail-closed guard: it checks each write against the rules you set and rejects a bad one by throwing, so no bytes ever reach the adapter. It's the simplest kind of wrap plugin - it vetoes rather than transforms.
Unlike compression() and encryption(), it never touches the body or writes any metadata, so there's nothing to undo on the way back out. It has no native dependencies and works on any adapter.
import { createFiles } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { validation } from "files-sdk/validation";
const files = createFiles({
adapter: s3({ bucket: "uploads" }),
plugins: [
validation({
maxSize: 10 * 1024 * 1024, // 10 MiB
allowedTypes: ["image/*", "application/pdf"],
key: /^[\w.-]+$/,
}),
],
});
await files.upload("photo.png", bytes); // ok
await files.upload("notes.txt", "..."); // throws: type not allowedWhat it checks
Every option is independent - set any combination, and with none set the plugin is a no-op pass-through.
| Option | What it does |
|---|---|
maxSize | Reject uploads larger than this many bytes. |
minSize | Reject uploads smaller than this many bytes - e.g. 1 to refuse empty files. |
allowedTypes | Reject uploads whose MIME type isn't in the list. |
key | Reject upload (and copy / move destinations) whose key fails the rule. |
Sizes
maxSize and minSize are byte counts. The check needs the body's length, so for an unknown-length stream the plugin buffers the body to measure it (the same trade-off the buffering plugins make). Known-length bodies - strings, Uint8Array, Blob, ArrayBuffer - are measured without a copy.
Types
Each allowedTypes entry is an exact type ("image/png") or a group wildcard ("image/*"). Matching is case-insensitive and ignores any ; charset=... parameter. The type checked is the one the object will be stored as: options.contentType if you pass it, otherwise a Blob/File's own .type, otherwise the type inferred from the key's extension.
validation({ allowedTypes: ["image/*"] });
await files.upload("photo.png", bytes); // ok - inferred image/png
await files.upload("doc.pdf", bytes); // throws - application/pdf not allowedKeys
key is either a RegExp the key must match or a predicate that returns true for keys you allow. Anchor your pattern (/^[\w.-]+$/) and don't use the g flag. The rule also guards the destination of copy and move, so a rename can't smuggle in a key your uploads would reject.
validation({ key: (key) => key.startsWith(`${tenantId}/`) });Ordering
Put validation() first so it vets the caller's original key and bytes before anything downstream transforms them:
plugins: [validation({ maxSize }), compression(), encryption(key)];Things to keep in mind
signedUploadUrl() hands upload capability to a client that writes
directly to the store, bypassing the plugin. When a size or type rule is
set, minting one would be a silent hole - so signedUploadUrl() fails
closed and throws. (A key-only policy still mints the URL, after checking
the requested key.)
- Reads,
url(),copy, andmovepass straight through. The plugin only guards writes; it transforms nothing, so there's nothing to reverse on download. - It stores no metadata. Nothing rides along on the object, so a validated bucket is indistinguishable from an unvalidated one - safe to enable or remove at any time.
- Size rules buffer unknown-length streams. Measuring a stream means draining it, which is incompatible with resumable uploads. Key and type rules never touch the body, so a key/type-only policy stays fully streaming.
- It's a guard, not a sanitizer. It vets the declared type and the key; it doesn't sniff magic bytes or rewrite anything. Pair it with
contentType()if you don't trust the client-declared type.
usage
Meter storage, bandwidth, and operation counts across a Files instance, and read the running totals back with files.usage(). Counts the bytes you actually read out of a download lazily, optionally bucketed per tenant or prefix. No native dependencies; works on any adapter.
versioning
Snapshot an object's prior bytes before every overwrite or delete, and roll a key back with versions() and restore(). Server-side copies under a hidden prefix - body-transparent, no native dependencies, works on any adapter.