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

@theokit/plugin-payments

v0.1.0

Published

Stripe-only payments plugin for TheoKit — defineStripeWebhook typed dispatcher + signature verification + idempotency (memory or @theokit/orm-backed) + createCheckoutSession helper (Stripe-hosted page passthrough). Form 4 Hybrid per plan p6-plugin-payment

Readme

@theokit/plugin-payments

Stripe-only payments plugin for TheoKit — typed webhook dispatcher with signature verification + idempotency + Checkout helper (hosted-page passthrough).

Status: v0.1.0 initial publish on the @next tag. Promote to @latest is calendar-gated alongside the Onda 2 cohort.

What you get

  • payments(opts) plugin factory wired into theo.config.ts.
  • defineStripeWebhook(type, handler) typed dispatcher — handler receives narrowed Stripe.Event variant via discriminated union on event.type.
  • Signature verification via stripe.webhooks.constructEvent() + actionable error type.
  • Idempotency store (memory default, swap for createOrmStore(repo) in prod) — prevents double-processing on Stripe retries (~3 days).
  • createCheckoutSession(client, params) returning {url, sessionId} for Stripe-hosted checkout.
  • Currency helpers (formatAmountForStripe, formatAmountForDisplay) — handles zero-decimal vs decimal currencies.
  • Selective Stripe type re-export for consumer ergonomics.

Stripe SDK is a required peer. @theokit/orm is optional — only needed when you swap the memory store for the production-grade orm-backed implementation.

Install

pnpm add @theokit/plugin-payments@next stripe
# Production idempotency via @theokit/orm:
pnpm add @theokit/orm@next drizzle-orm reflect-metadata

Wire it into theo.config.ts

import { payments } from "@theokit/plugin-payments";
import { defineConfig } from "theokit";

export default defineConfig({
  plugins: [
    payments({
      // secretKey defaults to process.env.STRIPE_SECRET_KEY
      // webhookSecret defaults to process.env.STRIPE_WEBHOOK_SECRET
      apiVersion: "2023-10-16",
    }),
  ],
});

Options reference

| Option | Type | Default | Notes | |---|---|---|---| | secretKey | string | process.env.STRIPE_SECRET_KEY | Stripe secret key | | webhookSecret | string | process.env.STRIPE_WEBHOOK_SECRET | Webhook signing secret | | apiVersion | Stripe.LatestApiVersion | '2023-10-16' | Stripe API version pin | | idempotencyStore | IdempotencyStore | memory store | Pass createOrmStore(repo) in prod |

Webhook handler example

import {
  defineStripeWebhook,
  processWebhook,
  WebhookRegistry,
  payments,
} from "@theokit/plugin-payments";

const plugin = payments();
const registry = new WebhookRegistry();

registry.register(
  defineStripeWebhook("checkout.session.completed", async (event) => {
    // event is typed as Stripe.CheckoutSessionCompletedEvent
    const session = event.data.object;
    console.log("Customer:", session.customer);
    // ...persist to your DB via @theokit/orm Repository
  }),
);

// In your theokit route handler (await req.text() FIRST — before any other body access):
export async function POST(req: Request) {
  const rawBody = await req.text();
  const result = await processWebhook({
    stripe: plugin.getStripeClient(),
    rawBody,
    signatureHeader: req.headers.get("stripe-signature") ?? undefined,
    webhookSecret: plugin.options.webhookSecret!,
    registry,
    store: plugin.options.idempotencyStore!,
  });

  switch (result.status) {
    case "ok":
      return Response.json({ received: true, eventId: result.eventId });
    case "signature_invalid":
      return Response.json({ error: result.message }, { status: 400 });
    case "handler_error":
      // Stripe retries on 5xx — choose carefully
      return Response.json({ error: "handler failed" }, { status: 500 });
  }
}

Checkout session example

import { createCheckoutSession, payments, formatAmountForStripe } from "@theokit/plugin-payments";

const plugin = payments();

// In your server action:
export async function startCheckout() {
  const { url, sessionId } = await createCheckoutSession(plugin.getStripeClient(), {
    mode: "payment",
    line_items: [
      {
        quantity: 1,
        price_data: {
          currency: "USD",
          product_data: { name: "Pro Plan" },
          unit_amount: formatAmountForStripe(29.99, "USD"),  // → 2999 cents
        },
      },
    ],
    success_url: "https://app.test/success?session_id={CHECKOUT_SESSION_ID}",
    cancel_url: "https://app.test/cancel",
    customer_email: "[email protected]",
    metadata: { userId: "u_123" },  // tie to your auth session
  });

  return { redirectTo: url, sessionId };
}

Idempotency in production

The memory store ships as default but is not multi-replica safe. For production, swap it for the orm-backed store:

import { createOrmStore, payments } from "@theokit/plugin-payments";
import { OrmModule, Repository } from "@theokit/orm";

// Schema (drizzle):
// CREATE TABLE webhook_events (
//   event_id TEXT PRIMARY KEY,
//   processed_at TIMESTAMP NOT NULL DEFAULT NOW()
// );

const repo = {
  async insertNew(eventId: string): Promise<boolean> {
    try {
      await db.insert(webhookEvents).values({ eventId });
      return true;
    } catch (err) {
      // UNIQUE constraint violation → already processed
      if (err.code === "23505") return false;
      throw err;
    }
  },
};

const plugin = payments({ idempotencyStore: createOrmStore(repo) });

Security threats addressed

| Threat | Mitigation | |---|---| | Replay attacks | Idempotency store rejects duplicate event.id via atomic UNIQUE constraint | | Signature forgery | stripe.webhooks.constructEvent() validates HMAC-SHA256 against webhook secret | | Body tampering | Signature verification consumes raw body BEFORE JSON parsing — see "Raw body access" below | | Secret leakage | secretKey + webhookSecret resolved from env vars; plugin never logs them | | Double-processing | Idempotency table guarantees each event.id runs exactly once |

Raw body access (critical)

Webhook routes MUST receive raw bytes BEFORE any other body access. JSON parsing before signature verification breaks the HMAC.

  • theokit / standard fetch handlers: await req.text() — no special config.
  • Vercel app router: works by default with req.text().
  • Vercel pages router: add export const config = { api: { bodyParser: false } } to the webhook route.
  • Cloudflare Workers: await request.text() — same.

Canonical subscription events to handle

When wiring subscription support, register handlers for these 7 events (no built-in state machine — your data model owns it):

| Event | When it fires | |---|---| | customer.subscription.created | New subscription activated | | customer.subscription.updated | Plan change, quantity update, etc. | | customer.subscription.deleted | Subscription cancelled | | customer.subscription.trial_will_end | 3-day trial-ending notification | | invoice.payment_succeeded | Successful charge → grant access | | invoice.payment_failed | Failed charge → revoke access / dunning | | checkout.session.completed | Initial purchase → bootstrap subscription |

Auth integration (G11)

Tie Stripe customers to your authenticated users via metadata:

await createCheckoutSession(client, {
  // ...
  customer_email: session.user.email,
  metadata: { userId: session.user.id },
});

In the webhook handler, read event.data.object.metadata.userId to correlate back. Plugin does NOT auto-correlate to avoid coupling to specific auth strategies.

License

MIT