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

@vibemonetize/server

v0.0.9

Published

Server-side SDK: requireEntitlement, meterUsage, verifyToken, getUser. Verifies entitlement JWTs locally via JWKS. Runs on Node and edge runtimes (web-standard APIs only).

Downloads

1,245

Readme

@vibemonetize/server

Server-side SDK for VibeMonetize. Verifies entitlement JWTs locally via JWKS, meters usage fire-and-forget, and reads the current user off a verified token. Runs on Node and edge runtimes — web-standard fetch/crypto.subtle (via jose) only, no node: imports.

All checks in this package fail closed: any invalid, expired, wrong-audience, or under-entitled token throws a typed VibeMonetizeError rather than silently allowing access. (The companion @vibemonetize/react package intentionally fails open in the UI when the API is unreachable — the enforcement of record always happens here.)

Install

pnpm add @vibemonetize/server

Quick start

import { createClient } from "@vibemonetize/server";

const monetize = createClient({ appId: process.env.VIBEMONETIZE_APP_ID! });

// Next.js Route Handler / any Node or edge server framework:
export async function POST(req: Request) {
  const token = req.headers.get("x-vibemonetize-token") ?? "";

  // Throws payment_required/forbidden/token_expired/unauthorized — let it bubble
  // to your error handler, or catch VibeMonetizeError and map `.code`/`.status`.
  const claims = await monetize.requireEntitlement(token, "export");

  // ... handle the request using `claims` ...

  void monetize.meterUsage({ meterSlug: "exports", endUserId: claims.sub });
  return new Response("ok");
}

No backend? Use the edge-function recipe

If your app is a pure client-side Vite build (the vibe-coding default), there is no server for this package to run on — and without it, every client-side gate is advisory. The fix is one copy-paste edge function (Vercel/Netlify) in front of your one expensive call: it reads x-vibemonetize-token, calls requireEntitlement/verifyToken here, returns 402 { code, message } on fail, and fires meterUsage server-side. Full recipe: docs.vibemonetize.dev/guides/edge-enforcement (raw-markdown agent version at /guides/edge-enforcement.md). The tested reference implementation is the demo app's guarded action: apps/demo/api/answer.tsapps/demo/server/answerHandler.ts.

Public API

createClient(options?)

createClient({
  apiUrl?: string;        // default: VIBEMONETIZE_API_URL env var, else "https://api.vibemonetize.dev"
  appId?: string;         // default: VIBEMONETIZE_APP_ID env var (required one way or another)
  secretKey?: string;     // default: VIBEMONETIZE_SECRET_KEY, falling back to VIBEMONETIZE_TEST_SECRET_KEY; sent as `Authorization: Bearer`
  issuer?: string;        // JWT `iss` to require; defaults to `apiUrl`
  verificationKey?: EntitlementSigningKey | JWTVerifyGetKey; // override JWKS fetch — mainly for tests
  expectMode?: Mode | "any"; // RFC 022 — enforce the token's mode claim; default derived from secretKey's prefix
  meterUsageMaxRetries?: number;     // default 3
  meterUsageRetryDelayMs?: number;   // default 200 (multiplied by attempt number)
}): VibeMonetizeServerClient

Returns a client with:

  • verifyToken(token): Promise<EntitlementTokenClaims> — verifies the JWT's signature (via the app's JWKS, cached), issuer, and audience (appId). Throws VibeMonetizeError("token_expired" | "unauthorized") on any failure. An empty/missing token throws unauthorized with a message calling out the likely cause — forgetting to forward the x-vibemonetize-token header your frontend's useEntitlementToken()/ getToken() produced — rather than a generic "invalid JWT". An issuer mismatch (the api's ENTITLEMENT_JWT_ISSUER doesn't match this client's issuer/apiUrl) or an audience mismatch (this client's appId doesn't match the token's — the token was issued for a different app) both name both sides of the mismatch and what to do about it, rather than a bare "Invalid entitlement token."

  • requireEntitlement(token, featureSlug): Promise<EntitlementTokenClaims> — verifies the token, then asserts featureSlug is granted. Throws:

    • token_expired / unauthorized — the token itself is invalid,
    • payment_required — the user has no active plan/meter grant at all (claims.plan === null AND features is empty AND meters is empty). This is keyed on the token's actual entitlements, never just on features happening to be empty — a credit-pack-only or meter-only customer has features: [] but a very real, paid plan claim, and is never told "no active plan" when their own token says otherwise,
    • forbidden — they have some active entitlement, just not this feature. The message lists the token's actual granted features (Missing feature "x". This token grants: a, b.) so a server-side feature-slug typo — which otherwise locks out every user with a message indistinguishable from a real denial — is obvious.
  • meterUsage(input): Promise<void> — fire-and-forget POST /v1/usage-events. Retries with backoff on network errors or a 5xx response; any 4xx (bad input, auth, unknown meter, or a usage cap already hit) gives up immediately without retrying. Either way the failure is then swallowed — the returned promise always resolves, so a metering hiccup never breaks your request handler. A definitive 4xx (e.g. a meter-slug typo) additionally logs one console.warn naming the meter slug and the API's error — it's almost always a coding mistake, not a transient blip, so it stays visible instead of vanishing along with the usage that never got recorded; a network error or 5xx never warns (only the exhausted-retries case for those is silent, by design — use meterUsageStrict if you need to react to those in code). input: { meterSlug, delta?, endUserId?, anonId?, idempotencyKey? }.

  • meterUsageStrict(input): Promise<{ accepted: boolean; remaining: number | null }> — same POST /v1/usage-events call, for when you need to enforce a usage cap instead of just recording it. Unlike meterUsage, it never swallows failure:

    • on success, resolves the parsed { accepted, remaining } body (remaining is null for uncapped meters),
    • on a 403 usage_limit_exceeded response, resolves { accepted: false, remaining: 0 } — treat this as your signal to show the paywall instead of serving the request,
    • on any other 4xx, throws the corresponding typed VibeMonetizeError (no retry),
    • on network errors or a 5xx response, retries with the same backoff as meterUsage, then throws.
    const usage = await monetize.meterUsageStrict({ meterSlug: "exports", endUserId: claims.sub });
    if (!usage.accepted) {
      return new Response("Usage limit reached", { status: 402 });
    }
  • getUser(token): Promise<{ id: string; plan: string | null; mode: Mode }> — convenience wrapper over verifyToken.

  • getMode(token): Promise<Mode> — convenience wrapper over verifyToken that returns just the verified mode claim.

  • mergeEndUser(input): Promise<{ endUserId: string }> (RFC 014) — for apps that bring their OWN verified auth (Neon Auth, Clerk, Supabase, ...) and want to attach a signed-in user's anonymous usage/memberships without a second email-code round trip. POSTs { appId, anonId, email } to the sk_-only POST /v1/end-users/merge (secretKey required) — you are attesting email is already verified by your own auth, so only call this with an email your own system has confirmed, never a raw user-supplied one. Idempotent: re-merging the same anonId is a no-op. Unlike meterUsage, it never swallows failure — throws the typed VibeMonetizeError from the response body on any non-2xx (e.g. not_found for an unknown app, unauthorized for a pk_ key), and network errors propagate. input: { anonId, email } (appId comes from the client's own config).

    // after your own auth confirms the user's (now-verified) email:
    const { endUserId } = await monetize.mergeEndUser({ anonId: previousAnonId, email });
    // switch the client/session to endUserId — same contract as the identify/verify flow.
  • grantCredits(input): Promise<{ creditGrantId: string; applied: boolean; remaining: number | null }> (#212) — idempotent server-side comp/goodwill credit grant onto a period:"credit" meter. POSTs to the sk_-only POST /v1/credits/grant (secretKey required — a pk_ key gets unauthorized); independent of both meterUsage/meterUsageStrict (consumption tracking) and Stripe checkout (a purchased credit pack) — use this for grants that never touch Stripe: support credits, refund goodwill, promos. Unlike meterUsage, it never swallows failure: retries network errors/5xx with the same backoff as meterUsage/meterUsageStrict (safe — the call is idempotent on idempotencyKey), then throws; any 4xx throws immediately, including VibeMonetizeError("idempotency_conflict") (409) if idempotencyKey was already used with a different meterSlug/delta/identity, or validation_failed if the meter isn't period:"credit". applied: false on the response means this call was an idempotent replay of an already-recorded grant (the credits were NOT granted again). input: { meterSlug, delta, endUserId?, anonId?, reason, idempotencyKey, metadata? } (appId comes from the client's own config; idempotencyKey is REQUIRED, unlike meterUsage's optional one).

    const { remaining } = await monetize.grantCredits({
      meterSlug: "credits",
      delta: 100,
      endUserId: claims.sub,
      reason: "refund goodwill — ticket #4821",
      idempotencyKey: "refund-4821",
    });
  • reserveUsage / settleUsage / releaseUsage (RFC 030) — secret-key-only, independently idempotent credit holds for long-running work. settleUsage accepts an optional actual amount and releases the unused portion; releaseUsage consumes nothing.

    const hold = await monetize.reserveUsage({
      meterSlug: "credits", amount: 1_000, endUserId: claims.sub,
      idempotencyKey: job.id, reason: "image batch",
    });
    if (!hold.accepted) throw new Error("Insufficient credits");
    await monetize.settleUsage({ reservationId: hold.reservationId, amount: 640, idempotencyKey: `${job.id}:settle` });

Also re-exports VibeMonetizeError and the EntitlementTokenClaims/ErrorCode/Mode/ UsageEventResponse/GrantCreditsResponse types from @vibemonetize/core so you don't need a direct dependency on it just to catch errors or type a claims object.

Developer test/live split (RFC 012)

Every secret key is either sk_test_... or sk_live_... (Stripe-style), and every entitlement JWT carries a mode: "test" | "live" claim stamped from the key used to mint it. createClient never parses or branches on the key's prefix — just pass whichever key you have (secretKey option, VIBEMONETIZE_SECRET_KEY env var, or its VIBEMONETIZE_TEST_SECRET_KEY fallback — see below) and use getUser(token).mode/ getMode(token) to tell which mode a given request ran in:

const user = await monetize.getUser(token);
if (user.mode === "test") {
  // e.g. skip an external side effect you only want to fire for real (live) usage
}

Tokens issued before this claim existed (or minted without an explicit mode) verify the same as always and report mode: "live" — this is purely additive.

createClient's env-var resolution for secretKey mirrors @vibemonetize/react's publishable-key fallback: it prefers VIBEMONETIZE_SECRET_KEY (live) and falls back to VIBEMONETIZE_TEST_SECRET_KEY whenever the live var is unset/blank. vibemonetize signup only ever mints a TEST pair, so this fallback is what makes meterUsage/ mergeEndUser/requireEntitlement work with zero edits right after signup — the same "zero edits" guarantee the client SDK already gave you.

expectMode — mode enforcement is on by default (RFC 022)

Every entitlement token carries a mode claim, and createClient now enforces it by default instead of merely surfacing it — this closes a real gap: test-mode checkout is a 4242 card away from free, so without enforcement any end user could mint themselves a test-mode "pro" entitlement and replay it against a live backend. The default is derived from your configured secretKey's own prefix:

  • sk_live_... (and grandfathered bare sk_...) -> requires mode: "live" tokens.
  • sk_test_... -> requires mode: "test" tokens.

A mismatch throws VibeMonetizeError("mode_mismatch") (403). Pass expectMode: "any" as the explicit escape hatch (e.g. a preview deployment intentionally serving both modes), or an explicit expectMode: "test" | "live" to override the derived default. A verification-only client (no secretKey at all) enforces nothing by default and logs a one-time console warning recommending you pass expectMode explicitly.

// A live-only backend: sk_live_... already implies expectMode: "live" — no code change.
const monetize = createClient({ appId, secretKey: process.env.STRIPE_LIVE_SECRET_KEY });

// A preview/staging deployment that intentionally serves both modes:
const monetize = createClient({ appId, secretKey, expectMode: "any" });

This is a breaking behavior change for existing integrators: a live client that previously silently honored test-mode tokens will now throw mode_mismatch on one. That is the vulnerability closing, not a regression — see this package's changelog.

Error codes you may see

token_expired (401), unauthorized (401), payment_required (402), forbidden (403), mode_mismatch (403, RFC 022) — see @vibemonetize/core's ERROR_CATALOG for the full list and default HTTP status codes.

Tests

pnpm test — vitest. Generates a local ES256 keypair, issues tokens with core's issueEntitlementToken, and verifies them via an injected verificationKey (bypassing the network JWKS fetch), plus meterUsage/meterUsageStrict retry/give-up/throw behavior against a mocked fetch.

SDK compatibility telemetry

API requests made by createClient include x-vibemonetize-sdk: server/<version>. The header contains no user data and is used to identify deployed SDK versions before changing the /v1 wire protocol.