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

@chattee.ai/blobs

v1.2.0

Published

File storage client for applications built on Chattee.

Readme

@chattee.ai/blobs

File storage for applications built on Chattee.

Container filesystems are rebuilt on every deploy, so anything written there is gone the next time the app ships. This stores files somewhere that survives — and, for public files, serves them from your app's own domain.

Setup

Nothing to configure. Adding the blob-storage resource to a project sets CHATTEE_API_URL, CHATTEE_PROJECT_TOKEN and CHATTEE_BLOBS_MAX_MB in the backend's environment; you never create, read or manage those.

import { blobs } from "@chattee.ai/blobs";

Server-side only. This module holds a secret from the backend's environment. Importing it into browser code would ship that secret to every visitor. There is deliberately no browser build.

Public and private files

Public files get a stable URL on your app's own domain, so they work in <img src>, in an Open Graph tag and in an email. Nothing you write sits on that path.

const { url } = await blobs.upload(fileStream, {
  filename: "avatar.png",
  visibility: "public",
});
// url === '/files/k3j2h1g0/avatar.png'

Private files are not reachable from any URL — there is no signed link, no token parameter, and no way to make one. Your backend fetches the bytes and serves them after checking whatever it needs to check. The decision stays yours; only the storage is ours.

const { key } = await blobs.upload(pdfStream, { filename: "invoice.pdf" });

app.get("/invoices/:id", requireLogin, async (req, res) => {
  const invoice = await db("invoices").where({ id: req.params.id }).first();
  if (invoice.user_id !== req.user.id) return res.sendStatus(403);

  const stream = await blobs.get(invoice.blob_key);
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader(
    "Content-Disposition",
    `attachment; filename*=UTF-8''${encodeURIComponent(invoice.filename)}`,
  );
  stream.pipe(res);
});

get() gives you a Node stream, so .pipe() is the whole of it. Note the filename*=UTF-8'' form: percent-encoding inside a plain quoted filename="..." is not decoded by browsers, so a file called my report.pdf would save as my%20report.pdf.

If the transfer fails partway the stream emits error, and pipe does not pass that on to your response — it is left open until something times out. Use pipeline(stream, res) from node:stream/promises if you would rather the response were torn down.

Store the key in your own table alongside whatever it belongs to. The key is how you find the file again; everything else about who may see it is your app's business.

API

| Method | | | -------------------------- | ------------------------------------------------------------------- | | upload(content, options) | store a file; returns { key, url, size, contentType, visibility } | | get(key) | the bytes as a Node Readable, for your app to serve | | getRange(key, range) | part of a file, so <video> and <audio> can seek | | getBuffer(key) | the bytes as a Buffer, for small files | | delete(key) | remove a file; false if it was not there | | list() | every file and the project's total usage | | publicUrl(key) | the URL of a public file | | isConfigured() | whether storage is set up, for a graceful fallback | | maxFileSizeBytes() | the per-file ceiling, so you can reject early |

upload(content, options)

content may be a stream, a Buffer, a Uint8Array or a string.

| Option | Default | | | ------------- | ------------- | ------------------------------------------------- | | filename | — | the original name; decides how the file is served | | key | generated | your own key, if you want one | | contentType | from filename | | | visibility | 'private' | 'public' to get a URL | | ownerUserId | — | your own user id, recorded with the file |

A generated key keeps the readable filename with a unique prefix, so two people uploading photo.png do not collide.

getRange(key, range)

A browser cannot seek in <video> or <audio> unless the server answers 206, and it will not ask unless your route passes its Range header on.

const { stream, status, headers } = await blobs.getRange(key, req.headers.range);
res.writeHead(status, {
  "Content-Type": headers.contentType,
  "Content-Length": headers.contentLength,
  ...(headers.contentRange && { "Content-Range": headers.contentRange }),
  "Accept-Ranges": "bytes",
});
stream.pipe(res);

status is 206 for a partial answer and 200 when the whole file came back — forward whichever you were given rather than assuming.

Two things the platform does not give back

The original filename. It is used to work out how the file is served and is not returned by get(). Keep it in your own table next to the key; that is where the rest of what you know about the file already lives.

The content type you declared. The stored type is derived from the filename's extension, never from what the uploader claimed — so list() can report something other than the contentType you passed to upload(). Set your response's Content-Type from your own row, as the example above does; that is your file and your users.

Errors

Everything throws ChatteeBlobError with a stable code:

| code | | | ---------------- | ---------------------------------------------------------------------------------- | | too_large | over this project's per-file limit — check maxFileSizeBytes() first to fail fast | | storage_limit | the project has used its whole allowance | | not_found | no such key | | no_storage | file storage is not enabled for this project | | unauthorized | the project token is missing or wrong | | not_configured | no Chattee configuration in the environment at all | | unreachable | the platform could not be reached |

import { ChatteeBlobError } from "@chattee.ai/blobs";

try {
  await blobs.upload(stream, { filename });
} catch (err) {
  if (err instanceof ChatteeBlobError && err.code === "too_large") {
    return res.status(413).json({ error: "That file is too big." });
  }
  throw err;
}

What is served inline

A public file's content type is derived from its extension by the platform, never from what the uploader claims. Images, PDFs, video and audio render in place; everything else downloads, including HTML and SVG. That is deliberate: files are served from your app's own domain, so a document that rendered in place could run script against your app with your users' sessions.

Install

npm install @chattee.ai/blobs

Requirements

Node 18 or newer (uses the built-in fetch and stream.Readable.fromWeb), and a project with the blob-storage managed resource.

Licence

MIT

Release notes

1.2.0

get(key) now returns a Node Readable, so stream.pipe(res) works. It used to return fetch's WHATWG ReadableStream, which has no .pipe() — so the documented way to serve a private file threw body.pipe is not a function and every private download answered 500. The documented shape was always the intended one; this makes the code agree with it.

Nothing you wrote needs to change: code written against the documentation starts working, and getBuffer(key) is unaffected. If you worked around this with Readable.fromWeb(...) of your own, remove the wrapper — fromWeb rejects a Node stream. If you consumed the result with pipeTo() or getReader(), those are web-stream methods and are gone; pipe(), pipeline() and for await all work.

New: getRange(key, range). Pass a browser's Range header through and forward the 206 it comes back with, so <video> and <audio> can seek in a private file. Previously the whole file was the only thing you could serve.

The documentation says two things it should always have said: the original filename is not returned by get() — keep it in your own table — and the stored content type is derived from the filename's extension rather than from the contentType you passed.

1.1.0

Adds AGENT.md to the published package. It is the model-facing core of this README — the same prose, without the install/requirements/licence/release-note sections — and it is what Chattee's build agent is given when a project uses this resource. Publishing it means the copy in node_modules always describes the version actually installed.

Nothing you wrote needs to change; there is no API change in this release.

1.0.1

CHATTEE_API_URL is now accepted with or without the /_chattee/v1 prefix. This client was already correct — it treats the binding as carrying the prefix, which is what the platform sets. The normaliser now sits in front of it so all three Chattee client packages agree on one rule, and a future change to the binding cannot break the one resource that always worked.

Nothing you wrote needs to change.

1.0.0

First release: upload, get, delete, list, and publicUrl. Public files get a stable URL on the app's own domain; private files are fetched by your backend and served under your own authorization.