npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@radonsdk/storage

v0.1.1

Published

One unified, provider-agnostic API for 25+ file-storage providers — object storage, CDN/media, self-hosted, and dev/local. Write storage.upload() once, swap providers via config. Signed URLs, resumable/chunked uploads, multi-provider failover, and bring-y

Readme

@radonsdk/storage

One unified, provider-agnostic API for 25+ file-storage providers — object storage, CDN/media, self-hosted, and dev/local. Write storage.upload() once and swap providers with a config change, never a code change.

Radon Storage gives you a single, typed interface (upload, download, delete, getUrl, list, exists, getMetadata, …) that every provider implements. Provider-specific quirks — auth schemes (AWS SigV4, Azure Shared Key, OSS V1, token headers), virtual-hosted vs path-style URLs, XML vs JSON responses, multipart protocols — are absorbed inside each adapter and never leak to your code. Own your data, no vendor lock-in.

  • 25+ providers, one API — S3, Cloudflare R2, Supabase, GCS, Azure Blob, Backblaze, Spaces, MinIO, Wasabi, Bunny, ImageKit, Cloudinary, and more.
  • Signed & public URLs — presigned GET/PUT for private objects, plain URLs for public ones, one getUrl().
  • Resumable / chunked uploads for large files, and multi-provider failover — both built in (Pro).
  • Bring-your-own-provider — implement one interface, register a slug.
  • Lazy-loaded adapters — an S3 + R2 app never bundles Azure's code.
  • Strict TypeScript, ESM + CJS, Node ≥ 18, zero required dependencies (every provider's wire protocol is hand-rolled — no @aws-sdk/*, no @azure/*, no @supabase/*).

Install

npm install @radonsdk/storage
# or: pnpm add @radonsdk/storage

Radon never stores your secrets. Each adapter reads credentials from RADON_<PROVIDER>_* environment variables (or from the providers config block, which takes precedence). See .env.example for every provider's variable names.


Quickstart (< 5 minutes)

import { RadonStorage } from "@radonsdk/storage";

const storage = new RadonStorage({
  providers: { s3: { bucket: "uploads", region: "us-east-1" } }, // creds from RADON_S3_*
  defaultProvider: "s3",
});

// Upload bytes, a stream, text, or straight from a file path:
const { key, url } = await storage.upload({ key: "avatars/ada.png", path: "./ada.png" });

// A time-limited signed URL for a private object:
const link = await storage.getUrl("avatars/ada.png", { signed: true, expiresIn: 3600 });

// Read it back, check it exists, get metadata, list, delete:
const bytes = await storage.download("avatars/ada.png");
await storage.exists("avatars/ada.png");        // true
await storage.getMetadata("avatars/ada.png");   // { size, contentType, lastModified, ... }
await storage.list("avatars/");                 // { objects, prefixes, cursor, hasMore }
await storage.delete("avatars/ada.png");

Switch providers without touching your upload code — it's a config change:

const storage = new RadonStorage({
  providers: {
    s3: { bucket: "uploads", region: "us-east-1" },
    r2: { bucket: "uploads", accountId: process.env.CF_ACCOUNT! },
  },
});

await storage.upload({ key: "a.png", body: buf }, { provider: "s3" });
await storage.upload({ key: "a.png", body: buf }, { provider: "r2" }); // same code, different store

Free vs. Pro

Three providers are free. The rest — and the resumable-upload and failover features — require a Radon Pro license key, verified once in await storage.init() and cached for the process lifetime.

| Tier | Capability | | --- | --- | | Free | s3, r2, local | | Pro (license) | every other provider (see catalog) + resumable/chunked upload + multi-provider failover + any bring-your-own provider |

const storage = new RadonStorage({
  providers: { supabase: { bucket: "media" } },        // a Pro provider
  licenseKey: process.env.RADON_LICENSE_KEY,           // or config.license: { key, verifyUrl, ... }
});

await storage.init();                                  // verifies the license (throws if invalid); unlocks Pro
await storage.upload({ key: "a.png", body: buf });
  • Free providers work with no license and no init().
  • Using a Pro provider/feature without a valid license throws LicenseRequiredError; a bad/unreachable key throws LicenseInvalidError (fail-closed).
  • Get a license at https://radonsdk.xyz/pricing. Introspect tiers with the exported FREE_PROVIDERS set and isProProvider(slug).

Free/Pro is a licensing concern at the API layer — every adapter, free or Pro, is built to the same completeness bar (real API calls, signed URLs, multipart where the provider supports it).


The unified API

// Upload: bytes / Uint8Array / stream / UTF-8 string via `body`, or a local file via `path`.
await storage.upload({ key: "docs/report.pdf", path: "./report.pdf", cacheControl: "public, max-age=31536000" });
await storage.upload({ key: "note.txt", body: "hello", metadata: { author: "ada" } });

// URLs — public or signed (GET download, or PUT for direct browser upload).
storage.getUrl("docs/report.pdf");                                  // public URL
await storage.getUrl("docs/report.pdf", { signed: true });          // signed GET (default 1h)
await storage.getUrl("up.bin", { signed: true, method: "PUT" });    // presigned upload URL
await storage.getUrl("r.pdf", { signed: true, download: "Report.pdf" }); // force download

// Read / inspect / enumerate.
await storage.download("note.txt");        // Buffer
await storage.exists("note.txt");          // boolean
await storage.getMetadata("note.txt");     // ObjectMetadata
await storage.list("docs/", { limit: 100, delimiter: "/" }); // page of objects + "folder" prefixes
await storage.copy("a.txt", "b.txt");      // server-side copy (where supported)

Operations a provider can't do throw a typed UnsupportedOperationError — never a silent failure — and provider.capabilities tells you up front what's available (signedUrls, multipart, publicUrls, list, …).


Resumable / chunked uploads (Pro)

Large files upload in parts, with automatic abort-on-failure so no half-finished object is left behind. Objects smaller than one part fall back to a single upload:

await storage.init(); // Pro feature — needs a license

await storage.uploadResumable(
  { key: "video/large.mp4", path: "./large.mp4" },
  { partSize: 8 * 1024 * 1024 }, // 8 MiB parts (the default)
);

Or drive the multipart primitives yourself (e.g. browser-chunked uploads):

const mp = await storage.createMultipartUpload("big.bin");
const p1 = await storage.uploadPart(mp, 1, chunk1);
const p2 = await storage.uploadPart(mp, 2, chunk2);
await storage.completeMultipartUpload(mp, [p1, p2]);

Available wherever capabilities.multipart is true (the S3 family, Azure).


Multi-provider failover (Pro)

Give an ordered chain of providers; each operation tries them in turn until one succeeds:

const storage = new RadonStorage({
  providers: {
    s3: { bucket: "primary", region: "us-east-1" },
    r2: { bucket: "backup", accountId: process.env.CF_ACCOUNT! },
  },
  failover: ["s3", "r2"],                        // try S3, fall back to R2
  licenseKey: process.env.RADON_LICENSE_KEY,
});
await storage.init();

await storage.upload({ key: "a.png", body: buf }); // lands on the first provider that accepts it

If every provider in the chain fails, an AllProvidersFailedError carries each underlying error in order. Naming a provider explicitly ({ provider: "s3" }) bypasses failover for that call.


Bring your own provider

Implement the StorageProvider interface (or extend BaseProvider, or S3CompatibleProvider for an S3-compatible store) and register it. Custom providers are Pro-gated.

import { BaseProvider, registerProvider } from "@radonsdk/storage";

class MyStoreProvider extends BaseProvider {
  readonly name = "my-store";
  readonly capabilities = {
    upload: true, delete: true, publicUrls: true, signedUrls: false,
    presignedUpload: false, list: true, metadata: true, multipart: false, copy: false,
  };
  async upload(input) { /* call your API via this.http(...) */ }
  async delete(key) { /* … */ }
  async exists(key) { /* … */ }
  async getMetadata(key) { /* … */ }
  async download(key) { /* … */ }
  getUrl(key, options) { /* … */ }
  async list(prefix, options) { /* … */ }
  native() { return this.http(""); }
}

registerProvider("my-store", async () => MyStoreProvider);

An S3-compatible store is even shorter — just resolve its endpoint:

import { S3CompatibleProvider, registerProvider } from "@radonsdk/storage";

class MyS3Provider extends S3CompatibleProvider {
  readonly name = "my-s3";
  protected readonly defaultRegion = "us-east-1";
  protected endpointFor({ region, bucket, customEndpoint }) {
    return { baseUrl: `${customEndpoint}/${bucket}`, region };
  }
}
registerProvider("my-s3", async () => MyS3Provider);

The native-client escape hatch

Radon is zero-dependency and hand-rolls each provider's wire protocol, so native() returns the adapter's own low-level machinery — its SigV4 signer, HTTP client, and resolved endpoint/bucket config — for anything the unified API doesn't cover:

const s3 = await storage.native("s3");
// s3.signer  → SigV4Signer      s3.http → HttpClient      s3.settings → { bucket, region, baseUrl, ... }
const url = s3.signer.presign({ method: "GET", url: s3.objectUrl("k"), expiresIn: 60 });

The signer (SigV4Signer, uriEncode), HTTP client (HttpClient), signature helpers (hmac, sha256Hex, safeEqual), and XML readers are all exported for building against a provider's raw API directly.


Provider catalog

25+ providers, fully implemented — real API calls, signed URLs, and multipart where the provider supports it. Import any adapter directly for an explicit dependency: import { S3Provider } from "@radonsdk/storage/providers/s3".

Object storage (S3-compatible)

| Slug | Provider | Tier | Notes | | --- | --- | --- | --- | | s3 | Amazon S3 | Free | virtual-hosted, SigV4, multipart, presigned URLs | | r2 | Cloudflare R2 | Free | path-style, region auto, multipart | | backblaze | Backblaze B2 | Pro | S3 API, regional cluster host | | spaces | DigitalOcean Spaces | Pro | regional host + CDN public URLs | | minio | MinIO (self-hosted) | Pro | path-style against your endpoint | | wasabi | Wasabi | Pro | regional S3 host | | linode | Linode / Akamai | Pro | regional cluster host | | vultr | Vultr Object Storage | Pro | regional host, signs us-east-1 | | ibm | IBM Cloud Object Storage | Pro | path-style, HMAC creds | | oracle | Oracle Cloud Storage | Pro | namespaced regional host | | scaleway | Scaleway | Pro | regional S3 host | | alibaba | Alibaba Cloud OSS | Pro | native OSS V1 signature | | gcs | Google Cloud Storage | Pro | S3-interop XML API + HMAC keys |

Self-hosted / niche

| Slug | Provider | Tier | Notes | | --- | --- | --- | --- | | ceph | Ceph RADOS Gateway | Pro | S3-compatible, self-hosted | | storj | Storj | Pro | decentralized, S3 gateway | | filebase | Filebase (IPFS) | Pro | IPFS-backed, S3 API | | tigris | Tigris | Pro | global S3, region auto | | seaweedfs | SeaweedFS | Pro | self-hosted S3 gateway |

Native-API object storage

| Slug | Provider | Tier | Notes | | --- | --- | --- | --- | | supabase | Supabase Storage | Pro | REST API, signed + public URLs | | azure | Azure Blob Storage | Pro | Shared Key auth, block multipart, SAS URLs |

Dev / simple

| Slug | Provider | Tier | Notes | | --- | --- | --- | --- | | local | Local filesystem | Free | files under a root dir, file:// or static URLs | | vercel-blob | Vercel Blob | Pro | token API, public URLs | | uploadthing | UploadThing-style | Pro | register→PUT handshake, CDN URLs |

CDN / media-focused

| Slug | Provider | Tier | Notes | | --- | --- | --- | --- | | bunny | Bunny.net Storage | Pro | AccessKey API, pull-zone + token URLs | | imagekit | ImageKit | Pro | multipart upload, ik-s signed URLs | | cloudinary | Cloudinary | Pro | signed upload, s--sig-- delivery URLs |

A few adapters have wire details flagged VERIFY vs live (bespoke signers / evolving APIs) — see DEFERRED.md.


Uploads, keys & metadata

  • Body: upload({ key, body }) accepts a Buffer, Uint8Array, a Node Readable stream, or a UTF-8 string. To upload from disk, pass path instead of body.
  • Keys are always bucket-relative; leading slashes are trimmed. list() supports a delimiter to get "folders" (common prefixes) instead of every key.
  • Content type is inferred from the key/path extension when you don't pass contentType.
  • Metadata (metadata: { … }) is stored as provider user-metadata (x-amz-meta-*, x-ms-meta-*, …) and returned by getMetadata().
  • Pagination: list() returns a cursor + hasMore; pass the cursor back as options.cursor for the next page.

Test vs. live

One flag hints sandbox vs production to providers that distinguish them (most object stores don't — a bucket is a bucket):

new RadonStorage({ mode: "test", providers: { /* … */ } }); // default
new RadonStorage({ mode: "live", providers: { /* … */ } });

Install size & lazy loading

The core (import { RadonStorage } from "@radonsdk/storage") contains zero provider code — only the interface, registry, SigV4 signer, HTTP/XML helpers, and the license/failover engines. Adapters are loaded on demand via dynamic import() the first time a provider is used, and each is emitted as its own subpath entry (@radonsdk/storage/providers/<slug>). A dev who configures only S3 + R2 never executes — or, under a code-splitting bundler, bundles — the other two dozen adapters. This is why one package can cover 25+ providers without bloating your install.


API surface

RadonStorage, createStorage, BaseProvider / S3CompatibleProvider, registerProvider / hasProvider / knownProviders, FREE_PROVIDERS / isProProvider / PRO_FEATURES, LicenseClient, runFailover, the SigV4Signer + HttpClient + signature/XML helpers (for BYO adapters), and every typed error (StorageError, LicenseRequiredError, UnsupportedOperationError, ObjectNotFoundError, AllProvidersFailedError, …). All types are exported.

License

MIT © Radon SDK. The SDK is MIT-licensed; the Pro tier requires a commercial license key at runtime for Pro providers and features. "# radonsdk-storage"