AbsoluteJS

Blob

@absolutejs/blobv0.6.2betaData & Sync

One BlobStore interface over local disk and every S3-compatible service, without a hard AWS SDK dependency.

Object storage substrate for the AbsoluteJS PaaS: one BlobStore interface (put, get, getStream, head, delete, list, presign) with adapters for local disk and any S3-compatible service. The S3 adapter takes a narrow S3ClientLike shape instead of depending on the AWS SDK directly, so the SDK stays out of your dependency tree until you need it. Other AbsoluteJS packages ride this substrate for uploads, replay artifacts, and media storage.

#Installation

BASH
bun add @absolutejs/blob

#Capabilities

Overview

Object storage substrate for AbsoluteJS. One BlobStore interface, multiple adapters, and bounded streaming for large artifacts.

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

Show 4 more

@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:

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

Writes accept maxBytes and an AbortSignal, including multipart S3/R2 uploads: blobs.put(key, body, { maxBytes, signal }).

Files at /. Metadata (contentType, user metadata, cache headers) at /.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)

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

R2 is fully S3-compatible — the only thing that changes is the endpoint. Same wiring + s3BlobStore adapter.

Show 2 more

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

put returns the stored object's metadata (size, contentType,

etag, user metadata).

get returns null for missing keys (not throw).

Show 6 more

getStream for large blobs — avoids loading the body into memory.

delete is idempotent: deleting a missing key is success.

list paginates via cursor — pass back into the next call as

options.cursor.

presign builds a time-limited URL for direct browser upload/

download. operation: 'put' for uploads, 'get' (default) for downloads. Throws BlobError('UNSUPPORTED') on local.

Key validation

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.

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.

One BlobStore interface

put, get, getStream, head, delete, list, and presign behave identically across adapters, so provider choice is a deploy-time detail.

S3-compatible adapter

AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi, and Tigris all work through the same s3BlobStore adapter — only the endpoint changes.

No hard SDK dependency

The adapter accepts a narrow S3ClientLike object, keeping @aws-sdk/client-s3 out of your dependency tree as a hard dependency.

Presigned URLs

presign builds time-limited URLs for direct browser upload (operation: put) or download (operation: get) without proxying bytes through your server.

Key validation built in

Every operation runs validateKey, rejecting leading slashes, NUL bytes, and . or .. path segments with BlobError(INVALID_KEY) — path traversal is closed at the substrate level.

Streaming and pagination

getStream serves large blobs without loading them into memory, and list paginates with a cursor you pass back into the next call.

Quarantine and inspection

Keep customer-controlled uploads private until bounded inspection returns clean. The ClamAV adapter distinguishes clean, infected, and unavailable; scanner errors never become a clean verdict.

#Adapter capability matrix

Choose an adapter by deployment and upload requirements without changing the BlobStore contract used by application code.

OptionLocalGeneric S3AWS SDKUploadThing
Streaming readsYesYesYesYes
Presigned transferNoYesYesSigned reads
Multipart uploadNoManualBuilt inProvider managed
Best fitDev/single hostS3-compatible cloudsAWS productionManaged uploads

#Safe upload lifecycle

Treat uploaded bytes as untrusted until bounded inspection succeeds.

  1. Quarantine
    Write to a quarantine key or isolated bucket.
  2. Bound
    Enforce byte, duration, and decompression limits.
  3. Inspect
    Run provider-neutral inspectors such as ClamAV INSTREAM.
  4. Decide
    Promote clean content or retain rejection evidence.

Outcomes

What you can build

Overview

Object storage substrate for AbsoluteJS. One BlobStore interface, multiple adapters, and bounded streaming for large artifacts.

Adapters

Subpath — Backs

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:

Hardening checklist

Production guidance

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.

Follow in order

Troubleshooting path

1
Key validation
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.
2
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.

#@absolutejs/blob quick start

Partial snippet

# @absolutejs/blob

TS
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 });

#Private upload inspection

Partial snippet

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:

TS
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,
});

#Local

Partial snippet

Working example for Local.

TS
import { localBlobStore } from "@absolutejs/blob/local";

const blobs = localBlobStore({ root: "./var/blobs" });
await blobs.put("uploads/file.pdf", body);

#Quick Start

Partial snippet

The local adapter stores files on disk — ideal for development, tests, and single-host deployments. get returns null for missing keys instead of throwing, and delete is idempotent.

TS
import { localBlobStore } from '@absolutejs/blob/local';

const blobs = localBlobStore({ root: './var/blobs' });

await blobs.put('users/42/avatar.png', body, {
	contentType: 'image/png'
});

const bytes = await blobs.get('users/42/avatar.png'); // null if missing
const meta = await blobs.head('users/42/avatar.png');
await blobs.delete('users/42/avatar.png'); // idempotent

#S3-Compatible Services

Partial snippet

The S3 adapter works with any S3-compatible service. You hand it an S3ClientLike object wired onto the real AWS SDK client, so the SDK is your dependency, not the substrate’s.

TS
import {
	S3Client,
	PutObjectCommand,
	GetObjectCommand,
	HeadObjectCommand,
	DeleteObjectCommand,
	ListObjectsV2Command
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { s3BlobStore } from '@absolutejs/blob/s3';

// Cloudflare R2 — swap the endpoint for B2, MinIO, Wasabi, Tigris
const aws = new S3Client({
	credentials: { accessKeyId, secretAccessKey },
	endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
	region: 'auto'
});

// Wire the narrow S3ClientLike shape onto the real SDK client
const blobs = s3BlobStore({
	bucket: 'my-bucket',
	client: {
		deleteObject: (input) => aws.send(new DeleteObjectCommand(input)),
		getObject: (input) => aws.send(new GetObjectCommand(input)),
		headObject: (input) => aws.send(new HeadObjectCommand(input)),
		listObjectsV2: (input) => aws.send(new ListObjectsV2Command(input)),
		presignGetObject: (input, options) =>
			getSignedUrl(aws, new GetObjectCommand(input), options),
		presignPutObject: (input, options) =>
			getSignedUrl(aws, new PutObjectCommand(input), options),
		putObject: (input) => aws.send(new PutObjectCommand(input))
	}
});

// Time-limited URL for direct browser upload
const url = await blobs.presign('uploads/file.pdf', {
	operation: 'put',
	ttlSeconds: 900
});

#Private Upload Inspection

Partial snippet

Inspect a quarantined object with strict byte bounds before promoting it into application-visible storage.

TS
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
});

if (result.verdict === 'clean') await promoteUpload(result);

#Adapters

Both adapters implement the same BlobStore interface, so swapping providers is a one-constructor change.

@absolutejs/blob/localFilesystem storage for dev, tests, and single-host deployments. Atomic writes via temp file + rename; metadata stored alongside each key.
@absolutejs/blob/s3AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi, Tigris — any S3-compatible HTTP API via a narrow S3ClientLike shape.
@absolutejs/blob/aws-s3Official AWS SDK wiring with multipart streaming uploads that never materialize the entire body in control-plane memory.
@absolutejs/blob/uploadthingUploadThing server SDK with stable application-owned custom ids and signed reads.
@absolutejs/blob/inspectionProvider-neutral stored-object inspection with bounded reads and a ClamAV clamd INSTREAM adapter.
Beta interface
@absolutejs/blob is 0.x — the BlobStore interface is settling, and option shapes may still shift between minor versions.
Presign requires an S3 backend
The local adapter throws BlobError(UNSUPPORTED) for presign. If you need presigned URLs in development, run the S3 adapter against a local MinIO.

Current package surface

What ships today

@absolutejs/blobv0.6.2 · betaData & SyncnpmSource
8entry points

Import surface · click to copy