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

@onesub/server

v0.13.0

Published

Server-side receipt validation middleware for react-native-iap. Apple StoreKit 2 + Google Play Billing. One line.

Readme

@onesub/server

Express middleware for Apple StoreKit 2 + Google Play Billing receipt validation, webhooks, and subscription/purchase storage. One line to mount.

npm install @onesub/server

Requirements

  • Node.js >= 20
  • Express as a peer dependency — ^4.17.0 || ^5.0.0:
    npm install @onesub/server express
  • Optional peer dependencies (install only what you use):
    npm install ioredis        # Redis stores, cache, webhook idempotency
    npm install bullmq         # Durable webhook queue with dead-letter list
    npm install pg             # Postgres stores
    npm install @opentelemetry/api   # Distributed tracing

Quick start

import express from 'express';
import { createOneSubMiddleware, PostgresSubscriptionStore, PostgresPurchaseStore } from '@onesub/server';

const app = express();

app.use(createOneSubMiddleware({
  apple: {
    bundleId: 'com.yourapp.id',
    sharedSecret: process.env.APPLE_SHARED_SECRET,
    // Optional — required only for the App Store Server API features below
    // (status fetch fallback, consumption response).
    keyId: process.env.APPLE_KEY_ID,
    issuerId: process.env.APPLE_ISSUER_ID,
    privateKey: process.env.APPLE_PRIVATE_KEY,
  },
  google: { packageName: 'com.yourapp.id', serviceAccountKey: process.env.GOOGLE_SERVICE_ACCOUNT_KEY },
  database: { url: process.env.DATABASE_URL },
  store:         new PostgresSubscriptionStore(process.env.DATABASE_URL),
  purchaseStore: new PostgresPurchaseStore(process.env.DATABASE_URL),
  // Optional:
  adminSecret: process.env.ADMIN_SECRET,   // enables /onesub/purchase/admin/*
  logger: require('pino')(),               // any { info, warn, error } logger
  refundPolicy: 'immediate',               // 'immediate' (default) | 'until_expiry'
}));

app.listen(4100);

Endpoints

| Route | Purpose | |------|---------| | POST /onesub/validate | Verify Apple/Google subscription receipt | | GET /onesub/status?userId= | Check subscription state | | POST /onesub/webhook/apple | App Store Server Notifications V2 | | POST /onesub/webhook/google | Google Play RTDN (Pub/Sub push) | | POST /onesub/purchase/validate | Verify one-time purchase (consumable / non-consumable) | | GET /onesub/purchase/status?userId= | List user's one-time purchases | | DELETE /onesub/purchase/admin/:userId/:productId | Wipe a non-consumable (requires adminSecret) | | POST /onesub/purchase/admin/grant | Manually grant a purchase (requires adminSecret) | | POST /onesub/purchase/admin/transfer | Reassign a transactionId to a new userId (requires adminSecret) | | GET /onesub/admin/webhook-deadletters | List failed webhook jobs in DLQ (requires adminSecret + BullMQ) | | POST /onesub/admin/webhook-replay/:id | Replay a dead-letter job (requires adminSecret + BullMQ) | | GET /openapi.json | OpenAPI 3.1 spec (opt-in via openapiHandler()) |

Lifecycle states (0.4.0+)

SubscriptionInfo.status carries the full lifecycle. The status route's active: boolean is computed as (active || grace_period) && expiresAt > now — but the raw status lets you render accurate UX:

| Status | active | UX hint | |--------|--------|---------| | active | ✅ | normal | | grace_period | ✅ | "결제 정보 확인 필요 (계속 사용 가능)" | | on_hold | ❌ | "결제 정보를 업데이트하세요" | | paused | ❌ | "재개 예정: {autoResumeTime}" | | expired / canceled | ❌ | re-purchase or restore |

See @onesub/shared README for the full mapping.

Refund policy (0.8.0+)

refundPolicy: 'immediate' | 'until_expiry'   // default 'immediate'
  • 'immediate' — subscription refunds (Apple REFUND/REVOKE, Google voided productType=1) flip status to canceled right away. Strict, fraud-resistant.
  • 'until_expiry' — keep status/expiresAt untouched, only flip willRenew = false. User keeps entitlement until the original expiry. Better UX for goodwill refunds.

IAP refunds (consumable / non-consumable) are always immediate regardless of policy — they have no expiry concept.

Optional Apple App Store Server API features (0.8.0+)

Set apple.keyId / apple.issuerId / apple.privateKey (PKCS8 ES256 from App Store Connect → Users and Access → Keys) to unlock:

Status API fallback (automatic)

If a webhook arrives for an originalTransactionId the store doesn't know (server downtime, queue truncation, fresh install), the webhook handler calls GET /inApps/v1/subscriptions/{originalTransactionId} to fetch canonical state from Apple and saves a record under a placeholder userId. Subsequent /onesub/validate from the host can claim ownership.

You can also call it directly:

import { fetchAppleSubscriptionStatus } from '@onesub/server';

const sub = await fetchAppleSubscriptionStatus(originalTxId, config.apple, { sandbox: false });
// sub: SubscriptionInfo | null  — null on missing creds / 404 / network failure

CONSUMPTION_REQUEST response hook

When Apple sends a CONSUMPTION_REQUEST notification (consumable refund review), without a hook Apple has no usage signal and tends to grant the refund. Provide a hook to PUT consumption info back to /inApps/v1/transactions/consumption/{txId}:

apple: {
  // ...
  consumptionInfoProvider: async (ctx) => ({
    customerConsented: true,                  // required; false makes Apple ignore the response
    consumptionStatus: 3,                     // 0=undeclared, 1=not consumed, 2=partial, 3=full
    deliveryStatus: 1,                        // 0=undeclared, 1=delivered & working, 2=quality issue, ...
    refundPreference: 2,                      // 0=undeclared, 1=grant, 2=decline, 3=no preference
    // see AppleConsumptionRequest for the full set of optional fields
  }),
}

Fire-and-forget; failures are logged but don't block the webhook 200.

Optional Google hooks (0.8.0+)

google: {
  // ...
  // Called when SUBSCRIPTION_PRICE_CHANGE_CONFIRMED (8) arrives — user agreed
  // to a price change; new price applies on next renewal. Useful for analytics.
  onPriceChangeConfirmed: async (ctx) => {
    await analytics.track('price_change_confirmed', ctx);
  },
}

Multi-instance deployments (0.12.0+)

By default every option uses in-process memory — zero config, zero infra. For multi-node clusters, swap in the Redis-backed equivalents:

import Redis from 'ioredis';
import {
  createOneSubMiddleware,
  RedisSubscriptionStore,
  RedisPurchaseStore,
  RedisCacheAdapter,
  RedisWebhookEventStore,
} from '@onesub/server';

const redis = new Redis(process.env.REDIS_URL!);

app.use(createOneSubMiddleware({
  // ...apple / google / database config...
  store:              new RedisSubscriptionStore(redis),
  purchaseStore:      new RedisPurchaseStore(redis),
  cache:              new RedisCacheAdapter(redis),      // share Apple JWT + Google OAuth tokens across nodes
  webhookEventStore:  new RedisWebhookEventStore(redis), // atomic SET NX dedup — one node handles each event
}));

| Class | Replaces | What it does | |-------|----------|--------------| | RedisSubscriptionStore | InMemorySubscriptionStore | Sorted-set index, newest-first pagination | | RedisPurchaseStore | InMemoryPurchaseStore | Cross-node purchase ownership | | RedisCacheAdapter | InMemoryCacheAdapter | Shared Apple JWT / Google OAuth token cache | | RedisWebhookEventStore | InMemoryWebhookEventStore | Atomic SET NX dedup (no get→set race) |

Webhook queue with durable retries

Replace the default in-process queue with BullMQ for decoupled retries and a dead-letter list:

import { BullMQWebhookQueue } from '@onesub/server';
import Redis from 'ioredis';

const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });
const webhookQueue = new BullMQWebhookQueue({ connection });

app.use(createOneSubMiddleware({
  // ...
  webhookQueue,
  adminSecret: process.env.ADMIN_SECRET, // required for DLQ endpoints
}));

The webhook route 200s immediately once the job is enqueued. The BullMQ worker retries up to 5× (exponential backoff, 1 s base) before moving the job to the dead-letter list. Inspect and replay via the admin endpoints above.

Cache adapter (0.12.0+)

The cache option controls how Apple JWT assertions and Google OAuth access tokens are cached between requests. Default is InMemoryCacheAdapter (process-local). Shared Redis cache prevents every cluster node from minting its own token:

import { InMemoryCacheAdapter, RedisCacheAdapter } from '@onesub/server';

// single instance (default — no config needed)
cache: new InMemoryCacheAdapter()

// multi-instance
cache: new RedisCacheAdapter(redis)
cache: new RedisCacheAdapter(redis, 'myapp:cache:') // custom key prefix

You can also pass any object implementing CacheAdapter:

export interface CacheAdapter {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
  del(key: string): Promise<void>;
}

OpenAPI (0.12.0+)

The full OpenAPI 3.1 document is exported as ONESUB_OPENAPI. Self-host it on your existing Express app:

import { openapiHandler } from '@onesub/server';

app.get('/openapi.json', openapiHandler());

Or import the spec directly for client generation:

import { ONESUB_OPENAPI } from '@onesub/server';
// use with openapi-typescript, swagger-ui-express, etc.

OpenTelemetry tracing (0.12.0+)

Install @opentelemetry/api alongside an OTel SDK and spans appear automatically for receipt validation and webhook processing. The helper is zero-overhead when the package is absent — no require error, no performance hit.

You can wrap your own code in the same tracer:

import { withSpan } from '@onesub/server';

const result = await withSpan('my-operation', { 'user.id': userId }, async () => {
  return await doWork();
});

Schema

Canonical Postgres DDL shipped at sql/schema.sql. Apply with psql -f or let store.initSchema() run it for you on startup.

store.initSchema() is safe to call on every boot — all DDL is IF NOT EXISTS. New columns added in later releases (e.g. linked_purchase_token, auto_resume_time) ship with ALTER TABLE IF NOT EXISTS so existing installs auto-backfill on the next startup.

Security

  • Apple JWS signature verified end-to-end against Apple Root CA G3 (as of 0.6.0)
  • Google RTDN: Authorization: Bearer JWT verified against Google JWKS when pushAudience is configured
  • transactionId ownership enforced — same receipt can't be reused across users (0.5.0+)
  • zod input validation + 50 KB body cap
  • Full write-up: docs/SECURITY.md
  • Error troubleshooting: docs/RECEIPT-ERRORS.md

Links

MIT © onesub contributors.