@absolutejs/blob
v0.6.2
Published
Object storage substrate for AbsoluteJS. One BlobStore interface with bounded streaming local and S3-compatible adapters, plus official AWS SDK wiring for multipart object storage.
Downloads
1,428
Maintainers
Readme
@absolutejs/blob
Object storage substrate for AbsoluteJS. One BlobStore interface, multiple
adapters, and bounded streaming for large artifacts.
const store: BlobStore = /* localBlobStore(...) | s3BlobStore(...) */;
await store.put('users/42/avatar.png', body, { contentType: 'image/png' });
const bytes = await store.get('users/42/avatar.png');
const url = await store.presign('users/42/avatar.png', { ttlSeconds: 900 });Adapters
| Subpath | Backs |
| ------------------------------ | ------------------------------------------------------------------------------------------------ |
| @absolutejs/blob/local | Filesystem (dev / single-host prod / tests) |
| @absolutejs/blob/s3 | AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi, Tigris — any S3-compatible HTTP API |
| @absolutejs/blob/aws-s3 | Official AWS SDK wiring, including multipart streaming uploads |
| @absolutejs/blob/uploadthing | UploadThing server SDK with stable application-owned custom ids and signed reads |
| @absolutejs/blob/inspection | Provider-neutral inspection contract, bounded stored-object inspection, and ClamAV clamd adapter |
Both implement the same BlobStore interface — swap providers with
one constructor change.
Private upload inspection
Uploads that can contain customer-controlled bytes should remain under a
private quarantine key until an inspector returns clean. The inspection
subpath preserves the same workflow across local, UploadThing, S3, R2, and
Spaces storage:
import {
createClamdBlobInspector,
inspectStoredBlob,
} from "@absolutejs/blob/inspection";
const inspector = createClamdBlobInspector({ host: "clamav.internal" });
const result = await inspectStoredBlob(blobs, inspector, {
filename: "evidence.pdf",
key: "quarantine/case/evidence.pdf",
maxBytes: 25 * 1024 * 1024,
});The clamd adapter uses the bounded INSTREAM protocol and returns clean,
infected, or unavailable. Scanner errors never become a clean verdict. The
host owns durable quarantine, retry, retention, and audit policy; Blob only
owns byte transport and normalized inspection.
Local
import { localBlobStore } from "@absolutejs/blob/local";
const blobs = localBlobStore({ root: "./var/blobs" });
await blobs.put("uploads/file.pdf", body);Writes accept maxBytes and an AbortSignal, including multipart S3/R2
uploads: blobs.put(key, body, { maxBytes, signal }).
Files at <root>/<key>. Metadata (contentType, user metadata,
cache headers) at <root>/<key>.meta.json. Atomic writes via temp
file + rename. presign() throws BlobError('UNSUPPORTED') —
use the S3 adapter against a local MinIO if you need presign in
dev.
S3 (any S3-compatible service)
import { S3Client } from "@aws-sdk/client-s3";
import { awsS3BlobStore } from "@absolutejs/blob/aws-s3";
const aws = new S3Client({ region: "us-east-1" });
const blobs = awsS3BlobStore({ bucket: "my-bucket", client: aws });awsS3BlobStore uses the official SDK command clients and
@aws-sdk/lib-storage multipart uploads. Streams are never materialized as one
control-plane buffer. The lower-level s3BlobStore and S3ClientLike remain
available for custom clients.
Cloudflare R2
const aws = new S3Client({
region: "auto",
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: { accessKeyId, secretAccessKey },
});R2 is fully S3-compatible — the only thing that changes is the
endpoint. Same wiring + s3BlobStore adapter.
Backblaze B2, MinIO, Wasabi, Tigris
All the same pattern. Point endpoint at the provider's URL,
provide credentials, hand the client into s3BlobStore.
BlobStore interface
type BlobStore = {
readonly description: string;
put: (
key: string,
body: BlobBody,
options?: PutOptions,
) => Promise<BlobObject>;
get: (key: string) => Promise<Uint8Array | null>;
getStream: (key: string) => Promise<ReadableStream<Uint8Array> | null>;
head: (key: string) => Promise<BlobObject | null>;
delete: (key: string) => Promise<void>;
list: (options?: ListOptions) => Promise<ListResult>;
presign: (key: string, options?: PresignOptions) => Promise<string>;
};
type BlobBody = Uint8Array | string | ReadableStream<Uint8Array>;putreturns the stored object's metadata (size, contentType, etag, user metadata).getreturnsnullfor missing keys (not throw).getStreamfor large blobs — avoids loading the body into memory.deleteis idempotent: deleting a missing key is success.listpaginates viacursor— pass back into the next call asoptions.cursor.presignbuilds a time-limited URL for direct browser upload/ download.operation: 'put'for uploads,'get'(default) for downloads. ThrowsBlobError('UNSUPPORTED')onlocal.
Key validation
validateKey("users/42/avatar.png"); // ok
validateKey("/etc/passwd"); // BlobError('INVALID_KEY')
validateKey("../escape"); // BlobError('INVALID_KEY')
validateKey("with\0nul"); // BlobError('INVALID_KEY')Adapters call validateKey() on every operation. Leading slashes,
NUL bytes, and . / .. path segments throw BlobError('INVALID_KEY')
— closes the path-traversal class of bugs at the substrate level.
License
BSL-1.1 with named carveout against hosted object-storage services. Change date: 2030-05-31 (Apache 2.0).
For private UploadThing storage, construct uploadThingBlobStore({token, acl: "private"})
and enable ACL overrides in the UploadThing app settings. The adapter explicitly
requests private uploads, enforces maxBytes while reading streams, and rejects
provider deletion failures so durable cleanup can retry. Application authorization
must precede every signed URL or download. collectBody(body, {maxBytes, signal})
is also available to adapters needing bounded buffering.
Durable malware inspection
@absolutejs/blob/inspection provides createClamdBlobInspector and
createBlobInspectionProcessor. Register the processor with your ABS Queue worker.
Persist a quarantine record and enqueue its { resourceId, revision } atomically.
The host load callback returns { key, filename, maxBytes } only for a current
quarantined revision; commit must recheck the same revision under a row lock.
Only a clean result may release a file. infected stays blocked. unavailable
is committed and then throws BlobInspectionUnavailableError, allowing queue retries.
Exhausted jobs must remain quarantined. Pass the worker cancellation signal.
Keep clamd on a private interface or loopback behind an authenticated tunnel: its TCP protocol does not authenticate clients. Maintain fresh signatures, configure archive/size limits to alert rather than silently skip, and monitor the daemon. The inspector bounds transport time and bytes; it cannot establish signature freshness or guarantee detection of every malicious file. Never expose unscanned provider URLs.
ClamAV can silently skip oversized ZIP entries even with AlertExceedsMax
(upstream issue #633). The Blob
inspector therefore independently checks ZIP/Office containers before accepting a
clean result: 25 MiB cumulative expansion, 1,000 entries, four nested levels, and
complete streams. Unsupported/malformed ZIPs or exceeded limits produce a blocked
Policy.ArchiveLimitsOrInvalid finding. This is a file policy finding, not proof of
malware. Non-ZIP formats continue to rely on the engine's format support and limits.
