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

@minhvuong/pirate-storage

v0.1.2

Published

Self-hosted, provider-oriented file storage helpers. Applications own their deployment, credentials, authentication, receipts, and database.

Readme

Drive storage SDK

Self-hosted, provider-oriented file storage helpers. Applications own their deployment, credentials, authentication, receipts, and database.

Packages

| Package | Purpose | | ------------------------------------------- | ----------------------------------------------------------------------- | | @minhvuong/pirate-storage | Core contracts, receipts, orchestration, hooks, Range parsing, journals | | @minhvuong/pirate-storage-discord | Discord webhook transport and production storage provider | | @minhvuong/pirate-storage-server | Typed file routes and Fetch/Worker upload handler | | @minhvuong/pirate-storage-client | Browser multipart uploader | | @minhvuong/pirate-storage-preview-browser | Optional JPEG/PNG preview Web Worker | | @minhvuong/pirate-storage-telegram | Telegram provider, media modes, variants, Range reads, signed receipts | | @minhvuong/pirate-storage-telegram-server | Long-lived Bun/mtcute runtime with SQLite session storage | | @minhvuong/pirate-storage-telegram-worker | Cloudflare Worker/DO runtime with durable session and upload journal |

All packages are self-hosted helpers. Applications own their credentials, upload routes, receipt database, and authorization policy.

Server or Worker

import { MemoryUploadJournal, createStorage } from "@minhvuong/pirate-storage";
import { createDiscordProvider } from "@minhvuong/pirate-storage-discord";
import {
  createFetchHandler,
  createFileRoute,
  defineFileRouter,
} from "@minhvuong/pirate-storage-server";

const discord = createDiscordProvider({
  webhooks: [env.DISCORD_WEBHOOK_1, env.DISCORD_WEBHOOK_2],
  strategy: "least-busy", // default; use "round-robin" for deterministic rotation
  manifestSecret: env.STORAGE_MANIFEST_SECRET,
  partSize: 8 * 1024 * 1024,
  attachmentsPerMessage: 4,
  uploadConcurrency: 3,
});

const storage = createStorage({
  providers: { discord },
  defaultProvider: "discord",
});

const f = createFileRoute();
const router = defineFileRouter({
  attachments: f({
    provider: "discord",
    accept: ["image/*", "video/*", "application/pdf"],
    maxFileSize: 2_000_000_000,
  })
    .input((value) => uploadInputSchema.parse(value))
    .middleware(async ({ request }) => {
      const user = await requireUser(request);
      return { userId: user.id };
    })
    .onUploadComplete(async ({ receipt, metadata }) => {
      // Prisma, Drizzle, D1, Convex, raw SQL, or anything else belongs here.
      return database.files.create({ receipt, ownerId: metadata.userId });
    }),
});

export const handleStorage = createFetchHandler({
  storage,
  router,
  journal: new MemoryUploadJournal(), // Development only; use durable storage in production.
});

The handler is Fetch-standard and can be mounted in a Cloudflare Worker, TanStack Start server route, Next route handler, Bun server, or any runtime that accepts Request and returns Response.

The Discord pool assigns one webhook to each complete upload. Receipt connectionId records the safe webhook identity without its token, allowing reads, resume, refresh, and deletion to route to the correct configured webhook after a restart. Keep the old webhook configured for as long as its receipts remain stored. For one connection, webhookUrl: env.DISCORD_WEBHOOK_URL remains supported.

Browser

import { createUploader } from "@minhvuong/pirate-storage-client";
import { createBrowserImagePreview } from "@minhvuong/pirate-storage-preview-browser";

const uploader = createUploader({
  endpoint: "/api/storage",
  headers: async () => ({ Authorization: `Bearer ${await getSessionToken()}` }),
  previewGenerators: [createBrowserImagePreview()],
});

const task = uploader.createUpload("attachments", file, {
  input: { folderId },
  concurrency: 3,
  onProgress: console.log,
});

task.pause();
task.resume();
const { receipt, serverData } = await task.done;

The browser uploads bounded parts to the application's authenticated handler. It never receives a Discord webhook or bot credential. Discord has no scoped presigned-upload mechanism.

Imperative API

const receipt = await storage.upload(file, { provider: "discord" });
const receipts = await storage.uploadMany(files, { provider: "discord" });
const remote = await storage.uploadFromUrl(url, { provider: "discord" });

await storage.stat(receipt);
await storage.verify(receipt);
const response = await storage.open(receipt, { range: "bytes=0-1048575" });
const httpResponse = await storage.toResponse(receipt, request);
const preview = await storage.preview(receipt, { width: 800, format: "webp" });
const freshLinks = await storage.refresh(receipt);
await storage.delete(receipt);

Grouped images

Discord and Telegram support 2–10 images as one logical group with one durable receipt:

const group = await storage.uploadGroup([cover, page1, page2], {
  provider: "discord", // or "telegram"
});

await storage.verifyGroup(group);
const image = await storage.openGroupItem(group, group.items[0].id);
const preview = await storage.previewGroupItem(group, group.items[0].id, { width: 640 });
await storage.deleteGroup(group); // deletes the complete group and its manifest

This first release treats the group as one lifecycle unit. It does not remove one item from an existing group. Upload independently when images need independent deletion.

  • Discord stores all images in one message and a separate signed manifest message. Each image has the configured partSize limit independently: 8 MiB by default, up to 10 MiB. Native image attachments may have metadata stripped, so group items conservatively report exactOriginal: false and expose their provider-side size under stored.
  • Telegram stores a native media album (Telegram represents it as grouped messages) and a separate signed manifest document. Each image must be JPEG, PNG, or WebP and at most 10 MiB.

Provider URLs, webhook tokens, bot tokens, and manifest secrets never enter the group receipt. Persist the complete group receipt JSON in the consumer database just like a normal receipt.

Providers declare capabilities instead of being forced to implement features they cannot support. Future providers can omit Range reads, multipart upload, deletion, preview generation, or transforms and expose those limitations through provider.capabilities.

Telegram

The runtime-specific package supplies the provider while the shared storage API remains unchanged:

const telegram = createTelegramServerProvider({
  apiId: env.TELEGRAM_API_ID,
  apiHash: env.TELEGRAM_API_HASH,
  botToken: env.TELEGRAM_BOT_TOKEN,
  channel: Number(env.TELEGRAM_CHANNEL_ID),
  channelId: env.TELEGRAM_CHANNEL_ID,
  sessionPath: ".data/telegram.sqlite",
  manifestSecret: env.STORAGE_MANIFEST_SECRET,
});

const storage = createStorage({ providers: { telegram: telegram.provider } });

await storage.upload(video, {
  provider: "telegram",
  providerOptions: {
    mode: "video",
    width: 1920,
    height: 1080,
    duration: 42,
    supportsStreaming: true,
  },
});

On Cloudflare, instantiate the Worker runtime inside one Durable Object and use DurableObjectUploadJournal. Browser parts default to 32 MiB, remain below common Worker request body limits, and are split internally into 512 KiB MTProto calls. Telegram combines those temporary parts into one final message.

The route builder maps validated input to provider-only settings without exposing credentials:

f({ provider: "telegram", accept: ["*/*"] })
  .input((value) => mediaInputSchema.parse(value))
  .providerOptions(({ input }): TelegramUploadOptions => input.media);

Production notes

  • Store the complete versioned receipt JSON in the consumer's database.
  • Never persist Discord CDN URLs; they are signed and expire.
  • Use at least 32 random bytes for STORAGE_MANIFEST_SECRET and back it up.
  • Replace MemoryUploadJournal with a Durable Object, D1, Redis, or another atomic durable adapter.
  • Protect every upload-session endpoint with application authentication.
  • Restrict uploadFromUrl destinations to avoid SSRF in server applications.
  • Discord image proxy transforms are experimental and should have an application fallback.
  • Telegram photo mode is transformed by Telegram; use document for exact original bytes.
  • Telegram multipart resume only lasts while Telegram retains temporary upload parts. Treat SESSION_EXPIRED as a restart signal.