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

@mediaos/sdk

v1.0.1

Published

Official TypeScript SDK for MediaOS — self-hosted media CDN platform. Upload, transform, and serve images, videos, and files.

Readme

@mediaos/sdk

Official TypeScript SDK for MediaOS — a self-hosted, multi-tenant media CDN platform.

Upload images, videos, and files. Get back optimized assets with instant resize URLs. No third-party dependencies.

Install

npm install @mediaos/sdk

Quick Start

import { MediaOS } from '@mediaos/sdk';

const mv = new MediaOS({
  url: 'https://cdn.yoursite.com',
  apiKey: 'mv_live_xxxxxxxxxxxxxxxxxxxx',
});

// Upload an image — returns the URL immediately
const result = await mv.upload(fileBuffer, { name: 'hero.jpg' });

console.log(result.url);       // https://cdn.yoursite.com/f/proj/hero-a8x3k2.webp
console.log(result.urls.thumb); // https://cdn.yoursite.com/img/fit/200/200/f/proj/hero-a8x3k2.webp
console.log(result.urls.md);    // https://cdn.yoursite.com/img/fit/800/0/f/proj/hero-a8x3k2.webp

Features

  • Upload — Single and bulk file uploads (images, videos, documents)
  • Auto-optimization — Images are converted to WebP, videos transcoded to H.264 MP4
  • Instant resize URLs — Get thumb, sm, md, lg variants automatically
  • Custom transforms — Build any resize URL with fit, fill, auto, or force modes
  • Signed URLs — Generate time-limited URLs for private files
  • File management — List, search, filter, and delete files
  • Usage tracking — Monitor storage, bandwidth, uploads, and downloads
  • Webhooks — Get notified on file.uploaded, file.processed, file.deleted, file.failed
  • Fully typed — Complete TypeScript definitions for all methods and responses
  • Zero dependencies — Uses native fetch and crypto only

Configuration

const mv = new MediaOS({
  url: 'https://cdn.yoursite.com', // Your MediaOS server URL
  apiKey: 'mv_live_...',           // API key from the dashboard
  timeout: 30000,                  // Request timeout in ms (default: 30000)
});

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | url | string | Yes | — | MediaOS server URL | | apiKey | string | Yes | — | Project API key | | timeout | number | No | 30000 | Request timeout in milliseconds |

Upload

Single file

import fs from 'fs';

const buffer = fs.readFileSync('./photo.jpg');

const result = await mv.upload(buffer, {
  name: 'photo.jpg',      // Display name (optional)
  folder: 'avatars',      // Organize into folders (optional)
  access: 'public',       // 'public' or 'private' (optional)
});

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "filename": "photo-a8x3k2.webp",
  "url": "https://cdn.yoursite.com/f/proj-id/avatars/photo-a8x3k2.webp",
  "storage_key": "proj-id/avatars/photo-a8x3k2.webp",
  "urls": {
    "original": "https://cdn.yoursite.com/f/proj-id/avatars/photo-a8x3k2.webp",
    "thumb": "https://cdn.yoursite.com/img/fit/200/200/f/proj-id/avatars/photo-a8x3k2.webp",
    "sm": "https://cdn.yoursite.com/img/fit/400/0/f/proj-id/avatars/photo-a8x3k2.webp",
    "md": "https://cdn.yoursite.com/img/fit/800/0/f/proj-id/avatars/photo-a8x3k2.webp",
    "lg": "https://cdn.yoursite.com/img/fit/1200/0/f/proj-id/avatars/photo-a8x3k2.webp"
  },
  "type": "image",
  "mime_type": "image/webp",
  "size": 45200,
  "original_size": 182400,
  "width": 1200,
  "height": 800,
  "access": "public",
  "status": "done",
  "processing_ms": 124,
  "created_at": "2026-03-05T10:30:00.000Z"
}

Bulk upload (up to 20 files)

const files = [
  { data: fs.readFileSync('./img1.jpg'), name: 'img1.jpg' },
  { data: fs.readFileSync('./img2.png'), name: 'img2.png' },
  { data: fs.readFileSync('./doc.pdf'),  name: 'doc.pdf' },
];

const result = await mv.uploadBulk(files, {
  folder: 'gallery',
  access: 'public',
});

console.log(result.uploaded); // 3
console.log(result.failed);   // 0
console.log(result.files);    // Array of UploadResult
console.log(result.errors);   // Array of { filename, error }

Browser upload (Blob/File)

// Works with File objects from <input type="file">
const input = document.querySelector('input[type="file"]');
const file = input.files[0];

const result = await mv.upload(file, { name: file.name });

File Management

List files

const files = await mv.files.list({
  page: 1,
  limit: 50,
  folder: 'avatars',          // Filter by folder
  type: 'image',              // 'image' | 'video' | 'file'
  search: 'hero',             // Search by filename
  sort: 'created_at',         // 'created_at' | 'size' | 'filename'
  order: 'desc',              // 'asc' | 'desc'
});

console.log(files.total);     // Total matching files
console.log(files.data);      // Array of file objects

Get file details

const file = await mv.files.get('550e8400-e29b-41d4-a716-446655440000');

console.log(file.url);
console.log(file.width, file.height);
console.log(file.original_name);

Delete file

const result = await mv.files.delete('550e8400-e29b-41d4-a716-446655440000');

console.log(result.deleted);     // true
console.log(result.freed_bytes); // 45200

Image Resizing

Images are resized on-the-fly via URL. No API call needed — just build the URL.

Pre-generated URLs

Every image upload returns ready-to-use resize URLs:

const result = await mv.upload(imageBuffer, { name: 'photo.jpg' });

result.urls.original; // Full size
result.urls.thumb;    // 200x200 fit
result.urls.sm;       // 400px wide
result.urls.md;       // 800px wide
result.urls.lg;       // 1200px wide

Custom resize URLs

// Fit within 300x300, preserving aspect ratio
mv.url(storageKey, { width: 300, height: 300, fit: 'fit' });

// Fill 500x500 (crop to fill)
mv.url(storageKey, { width: 500, height: 500, fit: 'fill' });

// Resize width only, auto height
mv.url(storageKey, { width: 600 });

// Square thumbnail shorthand
mv.thumbnailUrl(storageKey, 150);

Resize modes

| Mode | Description | |------|-------------| | fit | Resize to fit within dimensions, preserving aspect ratio (default) | | fill | Resize and crop to fill exact dimensions | | auto | Smart resize based on image content | | force | Force exact dimensions (may distort) |

Signed URLs

Generate time-limited URLs for private files:

// Default expiry (from server config)
const { url, expires_at } = await mv.files.signedUrl('file-id');

// Custom expiry: 1 hour (3600 seconds)
const { url, expires_at } = await mv.files.signedUrl('file-id', 3600);

// Use the signed URL — works until expires_at
// https://cdn.yoursite.com/f/proj/file.webp?token=abc123&expires=1709654400

Usage & Analytics

Current usage

const usage = await mv.usage.current();

console.log(usage.storage);    // { used: 52428800, limit: 1073741824, percent: 4.9 }
console.log(usage.bandwidth);  // { used: 104857600, limit: 10737418240, percent: 1.0 }
console.log(usage.uploads);    // 150
console.log(usage.downloads);  // 3200
console.log(usage.transforms); // 890
console.log(usage.files);      // { total: 150, images: 120, videos: 20, other: 10 }

Usage history (daily breakdown)

// Last 30 days (default)
const history = await mv.usage.history();

// Last 7 days
const history = await mv.usage.history(7);

for (const day of history.data) {
  console.log(day.date);           // "2026-03-05"
  console.log(day.uploads);        // 12
  console.log(day.download_bytes); // 5242880
  console.log(day.transforms);     // 45
}

Webhooks

Get notified when events occur in your project.

Events

| Event | Description | |-------|-------------| | file.uploaded | A file was uploaded successfully | | file.processed | Async processing (video transcode) completed | | file.deleted | A file was deleted | | file.failed | Async processing failed |

Create a webhook

const webhook = await mv.webhooks.create(
  'https://api.yoursite.com/webhooks/mediaos',
  ['file.uploaded', 'file.deleted']
);

console.log(webhook.id);     // Webhook ID
console.log(webhook.secret); // "whsec_..." — save this for verification

List webhooks

const { data: webhooks } = await mv.webhooks.list();

for (const wh of webhooks) {
  console.log(wh.url, wh.events, wh.success_count, wh.failure_count);
}

Delete a webhook

await mv.webhooks.delete('webhook-id');

Verify webhook signatures

When your server receives a webhook, verify it's from MediaOS:

import express from 'express';
import { MediaOS } from '@mediaos/sdk';

const app = express();
const mv = new MediaOS({ url: '...', apiKey: '...' });

app.post('/webhooks/mediaos', express.text({ type: '*/*' }), (req, res) => {
  const signature = req.headers['x-mv-signature'] as string;
  const webhookSecret = 'whsec_...'; // From webhook creation

  const valid = mv.webhooks.verify(req.body, signature, webhookSecret);

  if (!valid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log(event.event); // "file.uploaded"
  console.log(event.data);  // File data

  res.sendStatus(200);
});

Webhook payload format:

{
  "event": "file.uploaded",
  "timestamp": "2026-03-05T10:30:00.000Z",
  "project_id": "proj-uuid",
  "data": {
    "id": "file-uuid",
    "filename": "photo-a8x3k2.webp",
    "url": "https://cdn.yoursite.com/f/proj/photo-a8x3k2.webp",
    "type": "image",
    "size": 45200,
    "width": 1200,
    "height": 800
  }
}

Webhook headers:

| Header | Description | |--------|-------------| | X-MV-Signature | HMAC-SHA256 hex signature of the body | | X-MV-Event | Event name (e.g., file.uploaded) | | X-MV-Delivery-Id | Unique delivery ID | | X-MV-Timestamp | Unix timestamp of the delivery |

Error Handling

All API errors throw MediaOSApiError with code, status, and message:

import { MediaOS, MediaOSApiError } from '@mediaos/sdk';

try {
  await mv.files.get('nonexistent-id');
} catch (err) {
  if (err instanceof MediaOSApiError) {
    console.log(err.message); // "File not found"
    console.log(err.code);    // "NOT_FOUND"
    console.log(err.status);  // 404
  }
}

Error codes

| Code | Status | Description | |------|--------|-------------| | AUTH_REQUIRED | 401 | Missing or invalid API key | | FORBIDDEN | 403 | Key doesn't have required scope | | NOT_FOUND | 404 | Resource not found | | INVALID_SCOPES | 400 | Invalid scope in request | | NO_FILE | 400 | No file in upload request | | RATE_LIMITED | 429 | Rate limit exceeded | | ACCESS_DENIED | 403 | File is private, signed URL required | | URL_EXPIRED | 403 | Signed URL has expired | | TIMEOUT | 408 | Request timed out (client-side) |

TypeScript Types

All types are exported and available for use:

import type {
  MediaOSConfig,
  UploadOptions,
  UploadResult,
  BulkUploadResult,
  FileListOptions,
  FileListResult,
  FileMetadata,
  FileUrls,
  DeleteResult,
  SignedUrlResult,
  UrlOptions,
  UsageResult,
  UsageHistoryResult,
  WebhookResult,
  WebhookListResult,
} from '@mediaos/sdk';

Examples

Next.js / React — Upload with preview

'use client';
import { useState } from 'react';
import { MediaOS } from '@mediaos/sdk';

const mv = new MediaOS({
  url: process.env.NEXT_PUBLIC_CDN_URL!,
  apiKey: process.env.NEXT_PUBLIC_CDN_KEY!,
});

export default function ImageUploader() {
  const [url, setUrl] = useState('');

  async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    const result = await mv.upload(file, {
      name: file.name,
      folder: 'uploads',
    });

    setUrl(result.urls.md || result.url);
  }

  return (
    <div>
      <input type="file" accept="image/*" onChange={handleUpload} />
      {url && <img src={url} alt="Uploaded" />}
    </div>
  );
}

Express — Avatar upload endpoint

import express from 'express';
import multer from 'multer';
import { MediaOS } from '@mediaos/sdk';

const app = express();
const upload = multer({ storage: multer.memoryStorage() });

const mv = new MediaOS({
  url: 'https://cdn.yoursite.com',
  apiKey: process.env.CDN_API_KEY!,
});

app.post('/api/avatar', upload.single('avatar'), async (req, res) => {
  const result = await mv.upload(req.file.buffer, {
    name: `avatar-${req.user.id}.jpg`,
    folder: 'avatars',
  });

  // Save the URL to your database
  await db.users.update(req.user.id, {
    avatar_url: result.urls.md,
    avatar_thumb: result.urls.thumb,
  });

  res.json({ avatar: result.urls.md });
});

Node.js — Batch import from directory

import fs from 'fs';
import path from 'path';
import { MediaOS } from '@mediaos/sdk';

const mv = new MediaOS({
  url: 'https://cdn.yoursite.com',
  apiKey: 'mv_live_...',
});

const dir = './images';
const files = fs.readdirSync(dir)
  .filter(f => /\.(jpg|jpeg|png|gif|webp)$/i.test(f))
  .map(f => ({
    data: fs.readFileSync(path.join(dir, f)),
    name: f,
  }));

// Upload in batches of 20
for (let i = 0; i < files.length; i += 20) {
  const batch = files.slice(i, i + 20);
  const result = await mv.uploadBulk(batch, { folder: 'import' });
  console.log(`Batch ${i / 20 + 1}: ${result.uploaded} uploaded, ${result.failed} failed`);
}

Self-Hosting MediaOS

This SDK connects to a MediaOS server. To run your own:

git clone https://github.com/arrrrniii/MediaOs.git
cd mediaos
cp .env.example .env    # Configure your settings
docker compose up -d    # Start all services

Then create a project and API key via the dashboard at http://localhost:3001.

Requirements

  • Node.js 18+ (uses native fetch)
  • MediaOS server v1.0+

Also by ARN

SendMailOS — Email marketing platform built on AWS SES. Send campaigns, automate workflows, manage subscribers. 2,000+ free emails every month. Try it at sendmailos.com

License

MIT