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

@cubster/assets

v0.3.0

Published

Typed TypeScript SDK for the Cubster Assets API — keys, workspaces, uploads, grants, assets, and image/file URL builders, with an optional React subpath.

Readme

@cubster/assets

A typed, framework-agnostic TypeScript SDK for the Cubster Assets API — API keys, workspaces, uploads, client-upload grants, assets, and pure image/file URL builders. An optional @cubster/assets/react subpath adds a provider, hooks, and components.

The core (@cubster/assets) has zero React. React only enters when you import @cubster/assets/react.

Install

npm install @cubster/assets

react / react-dom are optional peer dependencies — you only need them if you use the /react subpath.

Core usage

Node / server (API key)

Authenticate with a csk_... key. It is sent as Authorization: Bearer <key> and is bound to a single workspace, which scopes every request.

import { createCubsterClient } from "@cubster/assets";

const cubster = createCubsterClient({ apiKey: process.env.CUBSTER_API_KEY });

// Upload a file (Node 18+ has File/Blob globals)
const buffer = await readFile("./photo.png");
const asset = await cubster.uploads.upload({
  file: new Blob([buffer], { type: "image/png" }),
  filename: "photo.png",
  name: "Marketing hero",
  tags: ["hero", "landing"],
});

// List + filter
const images = await cubster.assets.list({ type: "image", tags: ["hero"], limit: 20 });

// Update / delete
await cubster.assets.update(asset.id, { displayName: "Hero (v2)" });
await cubster.assets.delete(asset.id);

The API key is only ever placed on the Authorization header — the SDK never logs it. Keep it in an environment variable, never in client-side code.

Browser / dashboard (session cookie)

Omit apiKey and the client falls back to the first-party Netlify Identity session: requests are sent with credentials: "include" so the nf_jwt cookie rides along. A same-origin dashboard passes baseUrl: "".

import { createCubsterClient } from "@cubster/assets";

// Same-origin: relative requests, session-cookie auth.
const cubster = createCubsterClient({ baseUrl: "" });

const workspaces = await cubster.workspaces.list();
const keys = await cubster.apiKeys.list();
const created = await cubster.apiKeys.create({ label: "CI" });
console.log(created.key); // the full secret — shown exactly once

API key services

A key is scoped to one or more services — today "assets" (uploads/assets/serve URLs) and "transcription" — chosen at creation. apiKeys.create({ services: ["assets"] }) mints a key limited to just that service; omit services and the key gets all of them. ApiKey.services (list + create responses) and client.me.get()'s key-auth services field both report a key's current scope. Read the full registry (id, label, description, routes) with client.services.list():

const services = await cubster.services.list();
// [{ id: "assets", label: "Assets", description: "...", routes: [...] }, ...]

A request outside a key's scope 403s; read err.serviceDenied() (or the standalone getServiceDenied(err)) to get { service, enabled } without hand-parsing err.body. A key can't be re-scoped after creation — revoke and recreate it instead.

Client-upload grants (third-party browser upload)

A server holding a key mints a short-lived grant; a third-party browser uploads one file directly with only that grant (no key, no cookies).

// On your server (has the key):
const grant = await cubster.uploads.createGrant({
  maxSize: 5_000_000,
  mimeTypes: ["image/png", "image/jpeg"],
  ttlSeconds: 300,
});
// → send `grant` to the browser

// In the third-party browser (no key):
import { createCubsterClient } from "@cubster/assets";
const anon = createCubsterClient();
const asset = await anon.uploads.uploadWithGrant({ grant, file /* a File */ });

URL builders (pure)

No client needed — these mirror the API's serve-URL cascade exactly.

import { imageUrl, fileUrl, assetUrl } from "@cubster/assets";

imageUrl("key.png", { preset: "thumb" });               // /img/thumb/key.png
imageUrl("key.png", { width: 600, height: 400, fit: "cover" }); // /img/key.png/600/400/cover
fileUrl("doc.pdf");                                      // /f/doc.pdf

// Pick the right serve URL for an asset (image → /img, file/SVG → /f):
assetUrl(asset, { preset: "hero", baseUrl: "https://app.cubster.dev" });

Presets: thumb (150×150), avatar (256×256), hero (1200×675), small (w=300), medium (w=600), large (w=1200). The positional cascade is contiguous — /img/:key/:width/:height/:fit/:quality/:format — so you can't skip an earlier segment. SVGs and generic files are always served as forced downloads from /f/:key.

Errors

Every non-2xx response throws a typed CubsterError carrying the HTTP status and the API's { error } message.

import { CubsterError } from "@cubster/assets";

try {
  await cubster.assets.get("missing");
} catch (err) {
  if (err instanceof CubsterError && err.status === 404) {
    // handle not-found
  }
}

Transcription

transcribe() uploads an audio file and returns a durable, speaker-labelled transcript — the audio is kept as an asset (kind: "audio", served inline from /audio/:key), and the transcript (text, segments, words) lives alongside it. Cap: 4 MB per file (Netlify's synchronous-function body limit).

const transcript = await cubster.transcribe({
  file: new Blob([buffer], { type: "audio/mp4" }),
  filename: "standup.m4a",
  name: "Daily standup",
  language: "en", // BCP-47 tag; default "en"
  diarize: true, // default true
});

transcript.status; // "completed"
transcript.speakerCount; // 2
transcript.segments; // [{ speaker: 0, start, end, text, confidence }, ...]
transcript.words; // per-word timing (detail responses only)

List/get/delete/retry live under cubster.transcripts:

const transcripts = await cubster.transcripts.list({ status: "failed", limit: 20 });
// list items omit text/segments/words and carry a `preview` (first 200 chars) instead

const full = await cubster.transcripts.get(transcript.id, { workspace: "team-slug" }); // includes words; session auth targets a workspace, keys are workspace-bound and ignore it
await cubster.transcripts.delete(transcript.id); // also deletes the underlying audio asset

Failure + retry: the audio is never lost on a provider failure. transcribe() still stores the asset and inserts a failed/pending transcript row, then throws a CubsterError. Its body carries { error, transcript: { id, status, assetId, retryUrl } } — use err.transcriptFailure() (or the standalone getTranscriptFailure(err)) to read it without hand-parsing err.body:

import { CubsterError } from "@cubster/assets";

try {
  await cubster.transcribe({ file, filename: "clip.m4a" });
} catch (err) {
  if (err instanceof CubsterError) {
    const failure = err.transcriptFailure(); // { id, status, assetId, retryUrl } | null
    if (failure) {
      const retried = await cubster.transcripts.retry(failure.id);
    }
  }
}

A completed transcript can't be retried — transcripts.retry 409s in that case.

Realtime sessions

client.realtime.createSession() mints a short-lived (60s) connect token for a realtime voice session — a key-only call, and the key must include the realtime service. Hand { endpoint, token } straight to the broker that will open the WebSocket; Cubster never touches the audio itself.

const session = await cubster.realtime.createSession({ name: "Standup check-in" });
// { sessionId, endpoint, token, expiresAt }

// connect: WebSocket(session.endpoint, ...) with `Authorization: Bearer ${session.token}`
// on the upgrade request — see the broker's own docs for the wire protocol.

When the session ends, the broker posts the conversation transcript back to Cubster. It shows up like any other transcript (transcripts.list/get), but with no audio asset: assetId: null, source: { kind: "live", filename: null, contentType: null, size: null, durationSeconds, url: null }, and realtimeSessionId set to the session's id. TranscriptDetail-style UI should gate an <audio> player on source.url being non-null rather than assuming kind === "upload".

Diagrams

client.diagrams.* publishes agent-authored diagrams — bare SVG fragments drawn against the Cubster class vocabulary (cub-node, cub-edge, tones, etc.) — as versioned, workspace-scoped documents. Always preview() before you create(): it validates and rasterizes the same fragment without publishing anything, so you can look at the PNG and fix problems before they're public.

// 1. Draw, then look before you publish — writes nothing, even when valid.
const check = await cubster.diagrams.preview({ svg: mySvgFragment });
if (!check.valid) {
  console.error(check.errors); // [{ code, message, element? }, ...]
} else {
  await writeFile("preview.png", Buffer.from(check.png!, "base64"));
}
// 2. Publish — starts private (see the note below).
const diagram = await cubster.diagrams.create({
  title: "Auth flow",
  svg: mySvgFragment,
  kind: "flow",
  tags: ["auth"],
});
diagram.url; // https://app.cubster.dev/d/<slug> — 404s until it's shared
// List / fetch
const diagrams = await cubster.diagrams.list({ kind: "flow", includeArchived: false });
const latest = await cubster.diagrams.get(diagram.slug);
const v1 = await cubster.diagrams.get(diagram.slug, { version: 1 });
// Append a new version (an update IS a new version — svg is required)
const updated = await cubster.diagrams.update(diagram.slug, { svg: revisedSvgFragment });

// Archive is a reversible hide, never a delete.
await cubster.diagrams.archive(diagram.slug);
await cubster.diagrams.unarchive(diagram.slug);
// A semantic node/edge graph in, dagre-computed coordinates out. Writes nothing.
const layout = await cubster.diagrams.layout({
  direction: "LR",
  nodes: [{ id: "a", label: "Request" }, { id: "b", label: "Response" }],
  edges: [{ from: "a", to: "b", label: "200 OK" }],
});

// The full class vocabulary (classes, modifiers, typography, grid, tones, markers) as JSON.
const vocab = await cubster.diagrams.vocabulary();

Visibility is dashboard-only. A diagram always starts visibility: "private"; there is no field, param, or flag anywhere in this SDK to change it — flip it to unlisted/public from the Cubster dashboard. url/svgUrl/pngUrl are always present on a Diagram/DiagramSummary, but they 404 while private.

There is no delete. archive/unarchive are the only lifecycle mutations besides create/update — a hotlinked diagram (a GitHub README, say) should never 404 because someone tidied up.

React usage (@cubster/assets/react)

Wrap your app in a CubsterProvider with a client, then use the hooks and components.

import { createCubsterClient } from "@cubster/assets";
import {
  CubsterProvider,
  useAssets,
  useUpload,
  AssetImage,
  Uploader,
  Dropzone,
} from "@cubster/assets/react";

const client = createCubsterClient({ baseUrl: "" }); // dashboard: same-origin session

function App() {
  return (
    <CubsterProvider client={client}>
      <Gallery />
    </CubsterProvider>
  );
}

function Gallery() {
  const { assets, loading, error, refetch } = useAssets({ type: "image" });

  if (error) return <p role="alert">{error.message}</p>;

  return (
    <>
      <Uploader accept="image/*" onUpload={refetch} />
      <Dropzone accept="image/*" onUpload={refetch}>Drop images here</Dropzone>

      {loading || !assets ? (
        <p>Loading…</p>
      ) : (
        <ul>
          {assets.map((asset) => (
            <li key={asset.id}>
              <AssetImage asset={asset} preset="thumb" />
            </li>
          ))}
        </ul>
      )}
    </>
  );
}
  • useAssets(filters){ assets, loading, error, refetch }. assets is null until the first load resolves, so you can render a skeleton immediately.
  • useUpload(){ upload, uploading, error, asset, reset }. upload(input) resolves with the created asset and records it on the hook state.
  • <AssetImage asset|storageKey preset|transform … /> — renders an <img> off the URL builders, defaults alt from the asset, emits a responsive srcset for the default image case, and renders an optional fallback if the image fails to load.
  • <Uploader> / <Dropzone> — a file input / drag-drop target that upload via the client, with real loading + error states and onUpload(asset) / onError(err) callbacks. They ship no design-system styles — a className and data-* attributes are yours to target.

API surface

| Namespace | Methods | | --- | --- | | apiKeys | create, list, revoke | | services | list() | | workspaces | list, create | | uploads | upload, createGrant, uploadWithGrant | | assets | list, get, update, delete | | transcribe | transcribe(input) | | transcripts | list, get, delete, retry | | realtime | createSession(params?) | | diagrams | create, list, get(slug, params?), update(slug, params), archive, unarchive, preview, layout, vocabulary | | URL builders | imageUrl, fileUrl, rawImageUrl, assetUrl, imageCascadePath, imageSrcSet, assetSrcSet, assetStorageKey |

Limits (enforced by the API): max file size 25 MB; per-workspace storage cap 1 GB; image types png/jpeg/webp/gif/avif/svg+xml; file types pdf, text/plain, csv, markdown, json, zip; audio for transcription (mp3, m4a, aac, wav, webm, ogg, flac) capped at 4 MB per file.

License

MIT