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

@opa.sh/sdk

v0.4.0

Published

Official TypeScript/JavaScript SDK for the Opa link shortener API

Readme

@opa.sh/sdk

npm version CI license: MIT

Official TypeScript/JavaScript SDK for the Opa link shortener API (https://api.opa.sh/v1).

Resource-oriented, fully typed from the OpenAPI spec, universal runtime (Node, Bun, Browser, Cloudflare Workers, Deno), never throws — errors are always returned as values.

Install

npm install @opa.sh/sdk
# or: bun add @opa.sh/sdk
# or: pnpm add @opa.sh/sdk
# or: yarn add @opa.sh/sdk

Quick start

import { createOpaClient } from "@opa.sh/sdk";

const opa = createOpaClient({ apiKey: process.env.OPA_API_KEY! });

const { data, error } = await opa.links.create({
  destinationUrl: "https://example.com/black-friday",
  domain: "opa.sh",
  tagIds: ["tag_campaign2026"],
});

if (error) {
  console.error(error.code, error.message);
  return;
}

console.log(data.shortLink); // narrowed to success shape

Get your API key at app.opa.sh/settings/api-keys.

Authentication

Server-side only — never expose a key from a browser bundle:

const opa = createOpaClient({ apiKey: "opa_..." });

Base URL defaults to https://api.opa.sh/v1. Override it for staging or self-hosted instances:

const opa = createOpaClient({
  apiKey: "...",
  baseUrl: "https://api.staging.opa.sh/v1",
});

Errors — Result pattern

Every method returns { data, error }. The client never throws for HTTP errors — you're forced to handle them at the call site, and TypeScript narrows data to the success shape after the error check.

const { data, error } = await opa.links.get("lnk_xxx");

if (error) {
  // error.code is a typed union: "unauthorized" | "not_found" | "validation_error" | ...
  // error.status is the HTTP status (validation_error is always 422)
  // error.issues has field-level detail when error.code === "validation_error"
  switch (error.code) {
    case "not_found":
      return notFound();
    case "rate_limited":
      return retryLater(error.retryAfter); // ms, parsed from Retry-After
    default:
      return internalError(error);
  }
}

// data is narrowed to the success shape here
console.log(data.shortLink);

OpaError also exposes boolean getters for the common cases: isAuthError, isPermissionError, isValidationError, isRateLimitError. Use isOpaError(value) to narrow an unknown value.

Network failures (connection reset, DNS, timeout) also come back as error with code: "network_error" — the client never crashes on transient issues.

Resources

Links

// Create — only destinationUrl is required; domain falls back to the
// team's default domain when omitted.
const { data } = await opa.links.create({
  destinationUrl: "https://example.com",
  domain: "opa.sh",
  key: "custom-slug", // optional
  tagIds: ["tag_marketing"],
  password: "s3cret",
  expiresAt: "2027-01-01T00:00:00Z",
});

// Read
const { data } = await opa.links.get("lnk_xxx");

// Update — full-payload PATCH, same schema as create minus the id
const { data } = await opa.links.update("lnk_xxx", {
  destinationUrl: "https://example.com/new",
});

// Lifecycle
await opa.links.archive("lnk_xxx"); // reversible — the link keeps resolving
await opa.links.restore("lnk_xxx");
await opa.links.duplicate("lnk_xxx");

// List — single page (await it)
const { data } = await opa.links.list({ search: "marketing", limit: 50 });
console.log(data.items, data.hasMore, data.nextCursor);

// List — every page (for-await, memory-safe)
for await (const link of opa.links.list({ search: "marketing" })) {
  console.log(link.shortLink);
}

// Bulk operations
await opa.links.bulkArchive({ linkIds: ["lnk_a", "lnk_b", "lnk_c"] });
await opa.links.bulkRestore({ linkIds: ["lnk_a", "lnk_b"] });
await opa.links.bulkMove({ linkIds: ["lnk_a"], folderId: "fld_1" });
await opa.links.bulkTag({ linkIds: ["lnk_a"], tagIds: ["tag_q4campaign"] });

There is no tag list filter — use search, a case-insensitive substring match against the short link, destination URL, folder name, or tag name.

Analytics

from/to are required ISO-8601 dates on every analytics call (max 366-day range); there's no range: "7d" shorthand or interval parameter.

const { data } = await opa.analytics.summary({
  linkId: "lnk_xxx",
  from: "2026-08-12T00:00:00Z",
  to: "2026-08-19T23:59:59Z",
});

// One point per day over the same range/filters — no interval option.
const { data } = await opa.analytics.timeseries({
  linkId: "lnk_xxx",
  from: "2026-07-20T00:00:00Z",
  to: "2026-08-19T23:59:59Z",
});

// Raw click events — same await-one-page / for-await-every-page shape as opa.links.list
for await (const event of opa.analytics.events({ linkId: "lnk_xxx", limit: 100 })) {
  console.log(event.timestamp, event.country, event.device);
}

Both summary and timeseries accept the same optional filters: country, device, os, browser, refererDomain, utmSource, utmMedium, utmCampaign, variantUrl, domain (each a comma-separated "IN" filter).

Domains

const { data } = await opa.domains.list();

Not paginated — the list of domains an organization can shorten links under is small by nature.

Track

opa.track is stateless and safe to share across concurrent server requests. Every call carries its own identity; identify() never sets a "current user" on the client.

// Bind the anonymous id generated by @opa.sh/analytics to your own user id.
await opa.track.identify({
  anonymousId: "anon_123",
  externalId: "user_123",
  traits: { email: "[email protected]", plan: "pro" },
});

// Events may reference either an anonymous identity or an external identity.
await opa.track.event({
  eventId: "evt_checkout_123", // stable idempotency key
  eventName: "checkout_started",
  anonymousId: "anon_123",
  properties: { plan: "pro" },
});

// Legacy conversion contracts remain available server-side.
await opa.track.lead({
  clickId: "clk_123",
  eventName: "Signup",
  customerExternalId: "user_123",
});

await opa.track.sale({
  customerExternalId: "user_123",
  amount: 14990, // minor units (cents for BRL/USD)
  currency: "BRL",
  invoiceId: "inv_123", // stable idempotency key
});

The SDK automatically retries transient failures. Reuse the same eventId or invoiceId across retries and webhook redeliveries so the API can deduplicate the operation. This server SDK does not generate browser identities, cookies, sessions, consent state, or pageviews; forward an anonymousId captured by the browser integration when you need to reconcile anonymous history.

Retries and rate limiting

Automatic exponential backoff (with jitter) on 5xx and 429 responses, plus transport-level failures. Retry-After (seconds or HTTP-date) is honored when present, taking priority over the computed backoff. 4xx errors other than 429 are never retried.

const opa = createOpaClient({
  apiKey: "...",
  retry: {
    retries: 3, // default: 3
    minTimeoutMs: 500, // default: 500ms
    maxTimeoutMs: 8000, // default: 8000ms ceiling
  },
});

// Disable retries entirely:
const opa = createOpaClient({ apiKey: "...", retry: false });

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix seconds) headers — read them off the underlying Response if you need to react to them; the SDK doesn't currently surface a dedicated callback for this.

Custom fetch

For SSR, RSC, or edge runtimes that need a specific fetch implementation:

const opa = createOpaClient({
  apiKey: "...",
  fetch: customFetch, // must match the Fetch API signature
});

Useful for Next.js unstable_cache, Cloudflare Workers with request-scoped env.fetcher, or MSW test mocks.

TypeScript

Types are generated from the canonical OpenAPI spec (openapi/v1.json) via openapi-typescript. Autocomplete covers every documented path, method, body, query param, and response.

import type { OpaClient, OpaError, OpaErrorCode } from "@opa.sh/sdk";

Runtime support

  • Node.js 18+ (uses global fetch)
  • Bun
  • Deno
  • Cloudflare Workers (workerd)
  • Modern browsers — do not expose your API key in the browser bundle; proxy through your server

No dependencies at runtime beyond openapi-fetch (~2KB gzipped).

Bundle size

| Format | Size | Gzipped | | --- | --- | --- | | ESM | 5.3 KB | 2.0 KB | | CJS | 6.1 KB | 2.3 KB |

Tree-shakes cleanly — you only pay for the resources you import.

Contributing

The SDK is a wrapper around the auto-generated OpenAPI types. The generator (bun run generate:types) runs against openapi/v1.json, which is refreshed daily from https://api.opa.sh/v1/openapi via the sync-openapi workflow. When the spec adds a new endpoint, the sync workflow opens a PR you can extend with a hand-written resource wrapper.

git clone https://github.com/espocalabs/opa-sdk-js
cd opa-sdk-js
bun install
bun test
bun run build

Add a changeset before opening a PR:

bunx changeset

License

MIT © Espoca Labs