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 allowed

What it checks

Every option is independent - set any combination, and with none set the plugin is a no-op pass-through.

OptionWhat it does
maxSizeReject uploads larger than this many bytes.
minSizeReject uploads smaller than this many bytes - e.g. 1 to refuse empty files.
allowedTypesReject uploads whose MIME type isn't in the list.
keyReject 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 allowed

Keys

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, and move pass 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.

On this page