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

@rallya/sdk

v0.2.0

Published

JavaScript/TypeScript SDK for the Rallya event platform API

Readme

@rallya/sdk

Universal fetch-based JS/TS SDK for the Rallya event platform API (/api/v1).

  • Zero runtime dependencies. Node >= 18, browsers, edge workers.
  • Typed resources over every API domain: auth, orgs, events, tickets, orders, payments, attendees, check-in, audit, admin, health.
  • Automatic 401 → refresh → retry (single-flight) for user sessions.
  • src/generated/schema.ts is codegen from ../rallya/docs/swagger.json — never hand-edit. Run npm run gen.
  • src/types.ts holds hand-friendly types mapped 1:1 from the Go DTOs.

Install

npm i @rallya/sdk
import { RallyaClient } from "@rallya/sdk";

Node-only webhook helpers live under a second entrypoint (browsers must import @rallya/sdk only):

import { verifyStripeWebhookSignature } from "@rallya/sdk/node";

Auth: user sessions

For browsers, mobile apps, and anything acting as a logged-in user. Short-lived access JWT (15 min) + rotating refresh token (30 d), managed through a token store you provide (memory, localStorage, secure store — your choice).

import { RallyaClient, type TokenPair } from "@rallya/sdk";

let tokens: TokenPair | null = null;

const client = new RallyaClient({
  baseUrl: "http://localhost:8080/api/v1",
  getTokens: () => tokens,
  setTokens: (t) => { tokens = t; },
  onAuthFailure: () => { tokens = null; }, // refresh rejected: signed out
});

tokens = await client.auth.login({ email: "[email protected]", password: "secret123" });
const me = await client.auth.me();
await client.auth.logout(); // revokes server-side; clear tokens after
tokens = null;

baseUrl may include a trailing slash; org/event IDs accept UUID or slug (the SDK encodes segments for you).

Auth methods (client.auth)

| Method | Auth | Notes | |---|---|---| | register({ email, password, firstName?, lastName? }) | no | Creates the account; verify email before login | | login({ email, password }) → TokenPair | no | Store the pair via setTokens | | refresh(refreshToken) → TokenPair | no | Manual; auto-refresh already happens on 401 | | verifyEmail(token) / verifyEmailLink(token) | no | Link token (24 h) | | verifyCode(email, code) / resendVerification(email) | no | 6-digit OTP (10 min, 5 attempts) | | forgotPassword(email) | no | Always 200 (no account enumeration) | | resetPassword(token, newPassword) | no | Revokes all sessions | | logout() | yes | Revokes session + tokens | | me() → Me | yes | Profile + organizations[] |

Auth: API keys (server-to-server)

For backend jobs, cron, door tablets, and CI — anything that can't do a login + refresh dance. Keys are per-organization, long-lived, revocable, and sent as X-API-Key (no refresh cycle, no token store).

Mint a key with user auth (ADMIN+):

// one-off, with a user-authed client
const created = await client.request<{
  id: string; prefix: string; key: string;
}>("orgs/acme/api-keys", {
  method: "POST",
  body: { name: "door-tablet-1", scopes: ["checkin:create"], expiresAt: "2027-01-01T00:00:00Z" },
});
console.log(created.key); // raw secret, shown ONCE — save to env

Then use it:

import { RallyaClient } from "@rallya/sdk";

const client = new RallyaClient({
  baseUrl: "http://localhost:8080/api/v1",
  apiKey: process.env.RALLYA_API_KEY!, // rk_live_* ...
  onAuthFailure: () => { /* key revoked / expired */ },
});

// Same resource methods; X-API-Key is sent automatically.
await client.checkin.scan("acme", "fest-2026", { code: "qr-payload" });

A static string or an async provider both work:

new RallyaClient({ baseUrl, apiKey: () => vault.read("rallya/api-key") });

Rules that apply to every key:

  • Org-bound. A key for org A calling org B gets 403. Pass exactly one of apiKey / TokenStore — both together throws at construction.
  • Scopes. Snapshot of object:action grants (e.g. event:read, checkin:create, *:*). Non-empty scopes intersect the creator's live role; empty scopes inherit it. Unknown pairs are rejected at creation with 400.
  • Key management needs user JWT. Keys can never create/list/revoke keys (403) and can never touch /admin/*.
  • Fail-closed. Revoked/expired keys, removed creators, and suspended users all yield 401. Rotation = create new → dual-active grace → revoke old.

Resources

Orgs (client.orgs)

await client.orgs.create({ name: "Acme Inc", slug: "acme" });
await client.orgs.listMine();                       // Org[] with my role
await client.orgs.get("acme");                      // UUID or slug
await client.orgs.update("acme", { name: "Acme Corp" });
await client.orgs.remove("acme");                   // OWNER only

// members / invites / roles mirror the server's ADMIN+/OWNER gates
await client.orgs.listMembers("acme", { page: 1, perPage: 20 });
await client.orgs.invite("acme", { email: "[email protected]", role: "MEMBER" });
await client.orgs.defineRole("acme", {
  name: "door", permissions: [{ object: "checkin", action: "create" }],
});

Events (client.events)

// public discovery — no auth sent
await client.events.listPublic({ q: "jazz", status: "PUBLISHED", page: 1 });
await client.events.getPublic("fest-2026");

// org-scoped management
await client.events.create("acme", { title: "Fest", startsAt: "2026-10-01T18:00:00Z" });
await client.events.update("acme", "fest-2026", { venue: "Arena" });
await client.events.publish("acme", "fest-2026");
await client.events.unpublish("acme", "fest-2026");
await client.events.cancel("acme", "fest-2026");
await client.events.remove("acme", "fest-2026");
await client.events.uploadCover("acme", "fest-2026", file, "cover.png"); // ≤ 5 MB
await client.events.uploadImages("acme", "fest-2026", file, "stage.png");

Tickets (client.tickets)

await client.tickets.listPublic("fest-2026");
await client.tickets.list("acme", "fest-2026");
await client.tickets.create("acme", "fest-2026", {
  name: "GA", priceCents: 2000, quantityTotal: 100, maxPerOrder: 4,
});
await client.tickets.update("acme", "fest-2026", "tick_123", { priceCents: 2500 });
await client.tickets.activate("acme", "fest-2026", "tick_123");
await client.tickets.pause("acme", "fest-2026", "tick_123");
await client.tickets.remove("acme", "fest-2026", "tick_123");

Orders (client.orders)

Free orders confirm immediately; priced orders wait for payment. idempotencyKey is auto-generated when omitted — pass your own to make retries safe:

const order = await client.orders.create("fest-2026", {
  ticketTypeId: "tick_123",
  quantity: 2,
  idempotencyKey: "order-req-001", // optional
});
await client.orders.listMine({ page: 1, perPage: 20 });
await client.orders.get(order.id);
await client.orders.cancel(order.id);

Payments (client.payments)

// Priced orders: Stripe Checkout. Returns the redirect URL — complete
// payment in the browser. 503 when Stripe is unconfigured server-side.
const { url, sessionId } = await client.payments.checkout(order.id);
window.location.href = url;

Attendees (client.attendees)

await client.attendees.listMine();
await client.attendees.getMine(attendeeId);
await client.attendees.cancelMine(attendeeId);
await client.attendees.listRoster("acme", "fest-2026", { page: 1 });
await client.attendees.addWalkIn("acme", "fest-2026", { email: "[email protected]" });
await client.attendees.correct("acme", "fest-2026", attendeeId, { name: "Jane" });

Check-in (client.checkin)

Door scans never throw for refusals — they return 200 + outcome (CHECKED_IN, ALREADY_CHECKED_IN, INVALID_CODE, CANCELLED, WRONG_EVENT, REVERTED):

await client.checkin.scan("acme", "fest-2026", { code: "qr-payload" });
// or: { attendeeId }
await client.checkin.scanBatch("acme", "fest-2026", codes); // ≤ 50 codes
await client.checkin.revert("acme", "fest-2026", attendeeId);
await client.checkin.stats("acme", "fest-2026"); // { registered, checkedIn, ... }

Kits (client.kits)

Named kit types per event (merch/welcome packs) with per-attendee collections. Collects require a CHECKED_IN attendee (422 otherwise); reverting a check-in with active collections fails until voided:

await client.kits.create("acme", "fest-2026", { name: "VIP pack", quantityTotal: 100 });
await client.kits.list("acme", "fest-2026"); // with pending/collected/voided/remaining
await client.kits.collect("acme", "fest-2026", "kit_123", {
  attendeeId, idempotencyKey: "kit-req-001", // optional; reserve: true holds PENDING
});
await client.kits.markCollected("acme", "fest-2026", collectionId); // PENDING → COLLECTED
await client.kits.void("acme", "fest-2026", collectionId);          // frees re-issue
await client.kits.listCollections("acme", "fest-2026", { status: "COLLECTED" });

Subscriptions (client.subscriptions)

Org tiers (FREE/PRO/SCALE) gating organizer-side quotas and features. Attendee checkout stays one-off. Over-quota writes fail 402 (UPGRADE_REQUIRED); downgrades/cancels land at period end via the portal:

await client.subscriptions.listPlans(); // public catalog with limits + prices
await client.subscriptions.get("acme"); // absent subscription reads FREE
const { url } = await client.subscriptions.checkout("acme", "PRO"); // OWNER
window.location.href = url;
const { url: portal } = await client.subscriptions.portal("acme");  // OWNER

Audit / Admin / Health

await client.audit.listOrg("acme", { action: "member.added", page: 1 });
await client.audit.listPlatform({ org: "acme" }); // superadmin
await client.admin.listOrgs({ page: 1 });         // superadmin
await client.health.live();  // GET <origin>/health (outside /api/v1)
await client.health.hello(); // public

Pagination

List routes take { page?, perPage? } (server defaults 1/20, max 100) and return { items, total, page, perPage }:

const { items, total } = await client.events.listPublic({ page: 2, perPage: 50 });

Custom fetch

Pass fetchImpl to run on edge runtimes, tests, or instrumented clients:

new RallyaClient({ baseUrl, getTokens, setTokens, fetchImpl: myFetch });

Errors

Every API failure throws RallyaError ({ status, code?, message, details?, retryAfterMs? }). Stealth 404s mean "not found or no access" for orgs, events, and other people's orders — use the helper instead of treating 404 as absence:

import { RallyaError, isNotFoundOrForbidden } from "@rallya/sdk";

try {
  await client.orgs.get("acme");
} catch (e) {
  if (e instanceof RallyaError && e.isRateLimited) {
    await sleep(e.retryAfterMs ?? 1000); // 429 carries Retry-After
  }
  if (isNotFoundOrForbidden(e)) {
    // missing or no access — don't reveal which
  }
}

Stripe webhooks (Node)

Verify signatures over the raw request body (frameworks must not JSON-parse first). Needs STRIPE_WEBHOOK_SIGNING_SECRET:

import { verifyStripeWebhookSignature } from "@rallya/sdk/node";

const { verified, eventId } = verifyStripeWebhookSignature({
  rawBody, // string | Buffer, untouched
  signatureHeader: req.headers["stripe-signature"],
  webhookSecret: process.env.STRIPE_WEBHOOK_SIGNING_SECRET!,
});

Development

npm run gen        # regen src/generated/schema.ts from ../rallya/docs/swagger.json
npm run gen:check  # fail if committed schema drifted from swagger.json
npm test           # vitest
npm run build      # tsup → dist/