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

@assemora/media

v0.2.6

Published

Media library and storage adapters (local, S3-compatible)

Readme

@assemora/media

Media library and storage adapters.

Implementation phase: 7 — implemented.

useStorage(localStorage({ root: './storage/media' }))

await app.commands.execute('media.upload', { filename, mimeType, data })

The storage interface names no vendor, and both drivers SPEC.md §63 makes mandatory implement it. Each refuses a path that would climb out of where it may write — a filename arrives from an upload, and ../../etc/passwd is a filename.

The S3-compatible driver

useStorage(
  s3Storage({
    bucket: 'assets',
    region: 'auto',
    endpoint: 'https://<account>.r2.cloudflarestorage.com',
    accessKeyId: S3_KEY,
    secretAccessKey: S3_SECRET,
  }),
)

It talks to anything that speaks S3 — AWS, Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces — over fetch, signing every request with AWS Signature Version 4 computed here from node:crypto. There is no vendor SDK behind it: this package depends on @assemora/schema, @assemora/core and @assemora/data, and four HTTP requests are not a reason to widen that.

| Option | Meaning | | --- | --- | | bucket, region | Both are signed, so both must match the bucket's own. R2 wants region: 'auto'. | | accessKeyId, secretAccessKey | Required. An empty one fails at boot rather than with an unexplained 403 later. | | sessionToken | For temporary credentials. | | endpoint | The service. AWS S3 in region when it is left out. A path in the endpoint is kept, for a MinIO behind a reverse proxy. | | addressing | 'path' (default) or 'virtual-hosted'. | | publicUrl | A CDN or public bucket in front of the objects. | | signedUrlExpiresIn | Seconds a signed URL lives. One hour by default, seven days at most. Must be a finite number: Number(process.env.S3_URL_TTL) on an unset variable is NaN, and that is refused at boot rather than reaching the wire as X-Amz-Expires=NaN. | | logger | Where a refused request is explained. Pass the application's logger; the default writes to the console. | | fetch | Injected in tests, and wherever fetch is not global. |

Addressing

addressing is stated, never inferred from the endpoint's hostname. It defaults to 'path'https://host/bucket/key — because that is the one style every S3-compatible service understands, and the one that works for a bucket whose name is not a legal DNS label or contains a dot that a wildcard certificate will not cover. Set addressing: 'virtual-hosted' for AWS S3, which prefers https://bucket.host/key.

What url(path) returns

A bucket is private until somebody puts something public in front of it, so the answer depends on whether you did:

  • With publicUrl, the object's URL under that base, and nothing else. Use it for a CDN or a bucket you have deliberately made public.
  • Without it, a presigned GET URL, valid for signedUrlExpiresIn. The contract is url(path): string rather than a promise, and a presigned URL fits inside it: signing is a hash chain over values already in hand and waits for no I/O.

Two things follow from that, and neither is hidden:

  • A presigned URL expires. StorageDriver.url() has no way to say when, so a caller that stores one — in a cached response, a static build, an exported document — will find it stale. Studio and the REST layer ask for the URL as they render, so they are fine; anything that persists one should either use publicUrl or take the path and ask again. Saying so in the type means giving url() a return type richer than string, which changes StorageDriver for the local driver too. That is an ADR, not a patch, and it has not been written.
  • A presigned URL names the access key id in X-Amz-Credential. That is how SigV4 works and it is not a secret: it says which key the receiver should check the signature against. The secret access key never leaves the process — not in a URL, not in a header, not in an error, not in a log.

Errors

A refused request becomes an AssemoraError with code STORAGE_REQUEST_FAILED, status 502 and details: { operation } — and nothing else. The status is deliberately not the bucket's own: a 403 from S3 means this deployment's credentials or bucket policy are wrong, and repeating it would tell a perfectly authorized caller they are forbidden. That reasoning covers the whole payload rather than just the status, because AssemoraError.toPayload puts message and details on the wire verbatim and media.upload is reachable over REST: an upstream status or <Code> in either one is the same disclosure by another route, with a private bucket name and object key alongside it.

The diagnosis is not lost, it is moved. The bucket, the key, the HTTP status and the <Code> S3 returned go to logger.error('S3 request failed', …), where whoever owns the deployment reads them. Nothing else from the response body is kept even there — a SignatureDoesNotMatch body quotes the canonical request and the access key id back at you, which belongs in a terminal and not in a log shipper.

A key that is empty or contains a . or .. segment is refused with INVALID_PATH before any request is signed. There is no filesystem here, but .. is not inert either: path-style addressing puts the bucket in the path, and a URL resolves /bucket/../secret to /secret.

Content types

safeContentType decides what a browser may be told a file is, and both drivers reach the same answer — but at different moments. The local driver stores bytes and the application narrows the type as it serves them. An object store answers for itself, so this driver narrows the type on the way in: what a PUT writes is what the bucket returns forever after, and by then the application is no longer in the request path.

So an upload claiming image/svg+xml or text/html is stored as application/octet-stream with Content-Disposition: attachment, exactly as the local driver serves it. That is not paranoia about the bucket's own host: publicUrl is normally a CDN, a CDN is normally a subdomain of the application, and an SVG scripts whatever origin renders it (SPEC.md §85). The record in the library keeps the type the uploader claimed; only what the bucket is told changes.

Workspace dependencies

  • @assemora/schema
  • @assemora/core
  • @assemora/data