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

@eponyme/storage

v0.1.3

Published

Edge-compatible object storage drivers for Amazon S3, Cloudflare R2 and Google Cloud Storage.

Readme

@eponyme/storage

Edge-compatible object storage drivers for Amazon S3, S3-compatible providers, Cloudflare R2 and Google Cloud Storage – the last through its XML API and HMAC interoperability keys.

One small interface over four providers: put, get, delete, stat, list, move, url and presignPut. Written for Eponyme, which uses it for its media library, but it does not depend on it and never imports it. Anything that runs fetch can use it on its own.

Install

pnpm add @eponyme/storage

aws4fetch is the only runtime dependency. The drivers use fetch, ReadableStream, URL and Web Crypto, and do not import Node APIs.

Factories and credentials

A factory is created from the options that describe where things are stored, then called with the credentials that say who is storing them. Splitting the two is what lets the location live in a checked-in file while the secrets come from the environment.

import type { StorageFactoryContext } from '@eponyme/storage'
import { s3 } from '@eponyme/storage/s3'

const factory = s3({
  bucket: 'media',
  region: 'eu-west-3',
})

const context: StorageFactoryContext = {
  credentials: {
    accessKeyId: process.env.STORAGE_ACCESS_KEY_ID!,
    secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY!,
    sessionToken: process.env.STORAGE_SESSION_TOKEN,
  },
}

const storage = await factory(context)

Use short-lived, least-privilege credentials when the provider supports them. Never expose the factory context to browser code. Generated signed URLs are bearer credentials until they expire.

Amazon S3

Without an endpoint, S3 uses AWS virtual-host style:

import { s3 } from '@eponyme/storage/s3'

const factory = s3({ bucket: 'media', region: 'eu-west-3' })

Custom endpoints default to path style, which works for services such as MinIO and Scaleway. Set pathStyle: false when the provider requires a bucket subdomain.

const factory = s3({
  bucket: 'media',
  region: 'fr-par',
  endpoint: 'https://s3.fr-par.scw.cloud',
})

Cloudflare R2

R2 fixes the signing region to auto and requires the account endpoint:

import { r2 } from '@eponyme/storage/r2'

const factory = r2({
  bucket: 'media',
  endpoint: 'https://ACCOUNT_ID.r2.cloudflarestorage.com',
  publicUrl: 'https://media.example.com',
})

Google Cloud Storage

The GCS adapter uses https://storage.googleapis.com, the XML API, AWS4-compatible signatures and x-goog-* copy headers. Its accessKeyId and secretAccessKey must be a GCS HMAC interoperability key pair. Service-account JSON and OAuth credentials are not supported in this version.

import { gcs } from '@eponyme/storage/gcs'

const factory = gcs({ bucket: 'media' })

URLs and uploads

Signed URLs expire after 900 seconds by default. presignExpiresIn changes that default. Per-call download URLs can use expiresIn; accepted values are integers from 1 to 604800 seconds (seven days).

const inlineUrl = await storage.url('articles/cover.jpg')
const downloadUrl = await storage.url('exports/report.pdf', {
  expiresIn: 60,
  download: 'report.pdf',
})

const upload = await storage.presignPut?.('uploads/photo.jpg', {
  contentType: 'image/jpeg',
  size: 128_000,
})

When publicUrl is configured, url() returns that public origin for normal reads. Passing download deliberately returns a signed storage-origin URL so the response can override Content-Disposition.

presignPut() returns a PUT URL – and only a PUT; there is no POST policy form – with signed content-type and content-length headers, sent back as headers for the uploader to repeat. The uploader must send a body whose byte length exactly matches size; the storage provider rejects a different length because it no longer matches the signature. Browser uploads should pass a Blob or another body whose generated Content-Length matches the declared size.

Listing

list() walks a bucket one page at a time. Without a delimiter it returns every key under the prefix; with one it collapses everything past the next occurrence into prefixes, which is how a flat bucket is browsed as folders.

let cursor: string | undefined
do {
  const page = await storage.list('uploads/', { delimiter: '/', limit: 100, cursor })
  page.prefixes // ['uploads/2026/'] – the folders at this level
  page.objects // [{ key, size, lastModified }]
  cursor = page.cursor // absent on the last page
}
while (cursor)

limit accepts 1 to 1000, the provider's own page ceiling.

Errors

Every failure that comes from the provider is a StorageError carrying a code, so a caller can tell a missing object from an outage instead of matching on a message.

import { isStorageError } from '@eponyme/storage'

try {
  await storage.get(key)
}
catch (error) {
  if (isStorageError(error) && error.code === 'not_found') return null
  throw error
}

| code | Meaning | | --- | --- | | not_found | The object does not exist (404, 410). | | access_denied | The credentials are refused for this object (401, 403). | | unavailable | The provider answered but could not serve it (408, 429, 5xx). | | network | No answer at all; the request never completed. | | invalid_response | The provider answered something the driver cannot read. | | unknown | Anything else. |

A message never carries a response body, a signed URL or a credential – only the provider, the operation, the key and the status.

Invalid arguments stay TypeError and RangeError: a key with a .. segment is a bug in the caller, not a storage failure.

Limits

  • move() is a provider-side copy followed by delete, so S3's single-copy object-size limits apply.
  • The destination content type replaces the source metadata during a move.
  • GCS support is limited to the XML API with HMAC interoperability keys.
  • Signed URLs cannot live longer than seven days.
  • Object keys must be non-empty and relative, and cannot contain ., .., NUL or backslash segments.
  • stat() reports application/octet-stream for an object stored without a content type, which is common for a file put there by another tool.
  • put() and presignPut() are single PUT requests, so S3's 5 GiB per-request ceiling applies.

Optional live integration test

The default test suite never contacts a provider. To run the real cycle put -> stat -> get -> move -> presigned put -> delete, set the following variables and run pnpm test:integration:

EPONYME_STORAGE_PROVIDER=s3
EPONYME_STORAGE_BUCKET=media
EPONYME_STORAGE_REGION=eu-west-3
EPONYME_STORAGE_ENDPOINT=https://s3.example.com # optional for AWS
EPONYME_STORAGE_ACCESS_KEY_ID=...
EPONYME_STORAGE_SECRET_ACCESS_KEY=...
EPONYME_STORAGE_SESSION_TOKEN=... # optional
pnpm test:integration

Use EPONYME_STORAGE_PROVIDER=r2 with an endpoint, or gcs with GCS HMAC credentials. Set EPONYME_STORAGE_PATH_STYLE=false for a custom virtual-host endpoint.

Licence

MIT