npm package: @printrail/media
Description: Printrail Media SDK — upload, transform, and manage media assets
Author: John Christian Cabrera
Latest version: 0.2.0

# @printrail/media

TypeScript SDK for [Printrail Media](https://media.printrail.dev) — a self-hosted media storage and processing service built on AWS (S3, Lambda, CloudFront).

Upload files, auto-generate image variants, serve via CDN, and apply on-demand transforms. Drop-in replacement for Cloudinary.

## Install

```bash
pnpm add @printrail/media
# or
npm install @printrail/media
```

## Quick Start

```typescript
import { PrintrailMedia } from "@printrail/media";

const media = new PrintrailMedia({
  apiKey: "pm_live_...",
});

// Upload a file (browser, all-in-one)
const asset = await media.upload(file, {
  folder: "/photos",
  onProgress: (pct) => console.log(`${pct}%`),
});

console.log(asset.cdnUrl);           // Full-size CDN URL
console.log(asset.variants.thumb);    // 200x200 thumbnail
```

## Server / Client Split

The SDK enforces a security boundary — `PrintrailMedia` **only works server-side** (throws in the browser). This prevents API key leakage.

For frameworks like Next.js where uploads happen client-side but auth stays server-side:

```typescript
// --- Server action ---
import { PrintrailMedia } from "@printrail/media";

const media = new PrintrailMedia({ apiKey: process.env.PM_API_KEY! });

export async function getUploadUrl(fileName: string, contentType: string) {
  return media.presign({ fileName, contentType });
}

export async function confirmUpload(assetId: string) {
  return media.confirm(assetId);
}
```

```typescript
// --- Client component ---
import { uploadToPresignedUrl } from "@printrail/media/client";

const { uploadUrl, assetId } = await getUploadUrl(file.name, file.type);

await uploadToPresignedUrl(file, uploadUrl, file.type, {
  onProgress: (pct) => setProgress(pct),
});

const asset = await confirmUpload(assetId);
```

## Upload Methods

### `upload(file, options?)` — Browser, all-in-one

Handles presign, S3 PUT, and confirm in one call. Uses XHR for progress tracking.

```typescript
const asset = await media.upload(file, {
  folder: "/wedding/photos",
  tags: ["ceremony"],
  onProgress: (pct) => console.log(`${pct}%`),
});
```

### `uploadBlob(blob, fileName, contentType, options?)` — Universal

Works in Node.js and browsers. Uses `fetch` (no progress tracking).

```typescript
const blob = new Blob([buffer], { type: "image/png" });
const asset = await media.uploadBlob(blob, "screenshot.png", "image/png");
```

### `uploadDataUrl(dataUrl, fileName, options?)` — Base64

Upload from a data URL (e.g., canvas, QR codes).

```typescript
const asset = await media.uploadDataUrl(
  canvas.toDataURL("image/png"),
  "qr-code.png",
  { folder: "/qr-codes" },
);
```

### `presign(input)` / `confirm(assetId)` — Split flow

For custom upload flows where you control each step.

```typescript
const { uploadUrl, assetId } = await media.presign({
  fileName: "photo.jpg",
  contentType: "image/jpeg",
  fileSize: file.size,
  folder: "/uploads",
});

// Upload to S3 yourself (e.g., via XHR, fetch, or any HTTP client)
await fetch(uploadUrl, { method: "PUT", body: file, headers: { "Content-Type": "image/jpeg" } });

const asset = await media.confirm(assetId);
```

## Image Processing

Images are automatically processed after upload into three WebP variants:

| Variant  | Size      | Quality |
|----------|-----------|---------|
| `thumb`  | 200x200   | 70      |
| `medium` | 800x800   | 80      |
| `full`   | 1920x1920 | 85      |

Processing is async. Use `waitForReady()` if you need variants immediately:

```typescript
const asset = await media.confirm(assetId);
const ready = await media.waitForReady(asset.id, {
  timeout: 15_000,
  onPoll: (a) => console.log(a.status), // "processing" → "ready"
});

console.log(ready.variants.thumb);  // https://cdn.../slug/uuid/thumb.webp
console.log(ready.variants.medium); // https://cdn.../slug/uuid/medium.webp
console.log(ready.variants.full);   // https://cdn.../slug/uuid/full.webp
```

## CDN URLs

### Processed variants

```typescript
media.variantUrl("my-project/abc-123", "thumb");
// → https://d1eksnlsktdkcl.cloudfront.net/my-project/abc-123/thumb.webp

media.variantUrl("my-project/abc-123", "medium");
media.variantUrl("my-project/abc-123", "full");
```

### On-demand transforms

Generate any size/crop on the fly — results are cached at the CDN edge.

```typescript
media.transformUrl("my-project/abc-123", {
  width: 400,
  height: 300,
  crop: "fill",     // fill, fit, thumb, pad
  format: "webp",   // webp, jpeg, png, auto
  quality: 80,
  gravity: "center", // center, north, south, east, west
  blur: 10,
});
// → https://d1eksnlsktdkcl.cloudfront.net/t/blur_10,c_fill,f_webp,h_300,q_80,w_400/my-project/abc-123
```

### Shorthands

```typescript
media.thumbnail("my-project/abc-123");       // 200x200 thumb
media.thumbnail("my-project/abc-123", 100);  // 100x100 thumb
media.ogImage("my-project/abc-123");         // 1200x630 social image
```

## Assets CRUD

```typescript
// List with filtering + cursor pagination
const { assets, pagination } = await media.list({
  folder: "/wedding",
  mediaType: "image",
  status: "ready",
  tags: ["ceremony"],
  search: "cake",
  limit: 20,
});

// Get next page
if (pagination.hasMore) {
  const page2 = await media.list({ cursor: pagination.nextCursor });
}

// Get single asset
const asset = await media.get("asset-id");

// Update metadata
await media.update("asset-id", {
  tags: ["featured"],
  altText: "Wedding cake with flowers",
});

// Delete
await media.delete("asset-id");
```

## Client Exports

`@printrail/media/client` exports only browser-safe code (no API key):

```typescript
import { uploadToPresignedUrl, buildTransformUrl } from "@printrail/media/client";

// Upload with progress (XHR)
await uploadToPresignedUrl(file, presignedUrl, file.type, {
  onProgress: (pct) => setProgress(pct),
  signal: abortController.signal, // optional cancel
});

// Build transform URL (pure function, no network call)
const url = buildTransformUrl(
  "https://d1eksnlsktdkcl.cloudfront.net",
  "my-project/abc-123",
  { width: 400, crop: "fill" },
);
```

## Supported File Types

| Type      | Extensions                              | Max Size |
|-----------|-----------------------------------------|----------|
| Images    | jpg, jpeg, png, gif, webp, svg, avif, heic | 10 MB    |
| Videos    | mp4, mov, webm, avi, mkv                | 100 MB   |
| Documents | pdf, doc, docx, xls, xlsx, txt, csv     | 25 MB    |
| Audio     | mp3, wav, ogg, aac                      | 50 MB    |

## Configuration

```typescript
const media = new PrintrailMedia({
  apiKey: "pm_live_...",           // Required — your project API key
  apiUrl: "https://...",           // Optional — defaults to production
  cdnUrl: "https://...",           // Optional — defaults to production CDN
});
```

## License

MIT