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

@sesamy/capsule-server

v0.5.0

Published

Server-side encryption library for Capsule - CMS content encryption and subscription server integration

Readme

@sesamy/capsule-server

Server-side encryption library for Capsule - provides envelope encryption for content and subscription server utilities.

Installation

npm install @sesamy/capsule-server
# or
pnpm add @sesamy/capsule-server

Quick Start

The CMS server just needs a way to get keys - it doesn't care about tiers or how keys are derived.

import {
  createCmsServer,
  createTotpKeyProvider,
  createSubscriptionServer,
} from "@sesamy/capsule-server";

// Create a TOTP key provider (derives keys from master secret)
const totp = createTotpKeyProvider({
  masterSecret: process.env.MASTER_SECRET, // Base64-encoded 256-bit secret
});

// CMS side: encrypt content
const cms = createCmsServer({
  getKeys: (keyIds) => totp.getKeys(keyIds),
});

const encrypted = await cms.encrypt("article-123", premiumContent, {
  keyIds: ["premium", "enterprise"], // Just key IDs - CMS doesn't know what they mean
});

// Subscription side: handle unlock requests
const server = createSubscriptionServer({
  masterSecret: process.env.MASTER_SECRET,
});

// In your unlock endpoint
const result = await server.unlockForUser(wrappedKey, publicKey);

CMS Server

The CMS server encrypts content with envelope encryption. It doesn't know or care about subscription tiers - it just works with key IDs and calls your getKeys function to get the actual keys.

Creating the Server

import { createCmsServer } from "@sesamy/capsule-server";

// Option 1: Fetch keys from subscription server
const cms = createCmsServer({
  getKeys: async (keyIds) => {
    const response = await fetch("/api/keys", {
      method: "POST",
      body: JSON.stringify({ keyIds }),
    });
    return response.json();
    // Returns: [{ keyId: 'premium:123', key: 'base64...', expiresAt?: '...' }]
  },
});

// Option 2: Use TOTP key provider (derive keys locally)
const totp = createTotpKeyProvider({ masterSecret: process.env.MASTER_SECRET });
const cms = createCmsServer({
  getKeys: (keyIds) => totp.getKeys(keyIds),
});

Encrypting Content

const encrypted = await cms.encrypt("article-123", content, {
  keyIds: ["premium", "enterprise"], // Key IDs to encrypt with
});

Returns (JSON format):

{
  "articleId": "article-123",
  "encryptedContent": "base64...", // AES-256-GCM encrypted content
  "iv": "base64...", // 12-byte initialization vector
  "wrappedKeys": [
    {
      "keyId": "premium:1737158400",
      "wrappedDek": "base64...", // DEK wrapped with this key
      "expiresAt": "2025-01-18T01:00:00.000Z"
    },
    {
      "keyId": "premium:1737158430",
      "wrappedDek": "base64...",
      "expiresAt": "2025-01-18T01:00:30.000Z"
    }
  ]
}

Output Formats

// JSON (default) - for API responses
const data = await cms.encrypt(id, content, { keyIds: ["premium"] });

// HTML - ready to embed in your page
const html = await cms.encrypt(id, content, {
  keyIds: ["premium"],
  format: "html",
  htmlClass: "premium-content",
  placeholder: "<p>Subscribe to unlock...</p>",
});
// Result: <div class="premium-content" data-capsule='{"articleId":...}' data-capsule-id="article-123">
//           <p>Subscribe to unlock...</p>
//         </div>

// Template helper - get all formats at once
const { data, json, attribute, html } = await cms.encryptForTemplate(
  id,
  content,
  {
    keyIds: ["premium"],
  },
);

TOTP Key Provider

For deriving time-bucket keys locally from a shared master secret:

import { createTotpKeyProvider } from "@sesamy/capsule-server";

const totp = createTotpKeyProvider({
  masterSecret: process.env.MASTER_SECRET,
  bucketPeriodSeconds: 30, // Optional, default 30
});

// Get keys for given IDs (returns current + next bucket for each)
const keys = await totp.getKeys(["premium", "enterprise"]);
// Returns: [
//   { keyId: 'premium:1737158400', key: Buffer, expiresAt: Date },
//   { keyId: 'premium:1737158430', key: Buffer, expiresAt: Date },
//   { keyId: 'enterprise:1737158400', key: Buffer, expiresAt: Date },
//   { keyId: 'enterprise:1737158430', key: Buffer, expiresAt: Date },
// ]

// For per-article purchase keys (static, no expiration)
const articleKey = await totp.getArticleKey("article-123");
// Returns: { keyId: 'article:article-123', key: Buffer }

Combining with Article Keys

const cms = createCmsServer({
  getKeys: async (keyIds) => {
    const keys = await totp.getKeys(
      keyIds.filter((id) => !id.startsWith("article:")),
    );

    // Add article keys if requested
    for (const id of keyIds.filter((id) => id.startsWith("article:"))) {
      const articleId = id.slice(8);
      keys.push(await totp.getArticleKey(articleId));
    }

    return keys;
  },
});

// Now you can mix time-bucket and article keys
await cms.encrypt("article-123", content, {
  keyIds: ["premium", "article:article-123"],
});

Subscription Server

Handles unlock requests from users.

Creating the Server

import { createSubscriptionServer } from "@sesamy/capsule-server";

const server = createSubscriptionServer({
  masterSecret: process.env.MASTER_SECRET,
  bucketPeriodSeconds: 30,
});

Unlock Endpoint

app.post("/api/unlock", async (req) => {
  // Validate user subscription here!
  const { keyId, wrappedDek, publicKey } = req.body;

  return server.unlockForUser(
    { keyId, wrappedDek },
    publicKey,
    // Optional: lookup for static keys (per-article purchase)
    (keyId) => staticKeyStore.get(keyId),
  );
});

Share Links & Pre-signed Tokens

Capsule supports pre-signed tokens for sharing content without requiring user authentication. This is perfect for:

  • 📱 Sharing articles on social media (Facebook, Twitter, LinkedIn)
  • 📧 Email campaigns with direct unlock links
  • 🎁 "Gift this article" features
  • ⏰ Time-limited promotional access

How Share Links Work

┌─────────────────────────────────────────────────────────────────────────────┐
│                         SHARE LINK FLOW                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  PUBLISHER                         READER                    SERVER         │
│  ─────────                         ──────                    ──────         │
│      │                                │                          │          │
│      │ 1. Generate token              │                          │          │
│      │    (tier, expiry, maxUses)     │                          │          │
│      │─────────────────────────────►  │                          │          │
│      │                                │                          │          │
│      │ 2. Create share URL            │                          │          │
│      │    ?token=eyJhbGc...           │                          │          │
│      │                                │                          │          │
│  ════╪════════════════════════════════╪══════════════════════════╪════════  │
│      │     (Share on social media)    │                          │          │
│  ════╪════════════════════════════════╪══════════════════════════╪════════  │
│                                       │                          │          │
│                     3. Click link ───►│                          │          │
│                                       │                          │          │
│                                       │ 4. Extract token from URL│          │
│                                       │    Generate RSA keypair  │          │
│                                       │                          │          │
│                                       │ 5. POST /api/unlock ────►│          │
│                                       │    { token, wrappedDek,  │          │
│                                       │      publicKey }         │          │
│                                       │                          │          │
│                                       │                 6. Validate token   │
│                                       │                    Check expiry     │
│                                       │                    Log analytics    │
│                                       │                          │          │
│                                       │◄─────────────── 7. Return DEK       │
│                                       │                    (wrapped for     │
│                                       │                     client key)     │
│                                       │                          │          │
│                                       │ 8. Decrypt content       │          │
│                                       │    Display article ✨    │          │
│                                       │                          │          │
└─────────────────────────────────────────────────────────────────────────────┘

Creating Tokens

import { createTokenManager } from "@sesamy/capsule-server";

const tokens = createTokenManager({
  secret: process.env.TOKEN_SECRET, // Separate from master secret
});

// Generate a share token
const token = tokens.generate({
  tier: "premium", // Required: which tier to grant access to
  expiresIn: "7d", // Required: "1h", "24h", "7d", "30d"
  articleId: "crypto-guide", // Optional: restrict to specific article
  maxUses: 1000, // Optional: limit total uses
  userId: "publisher-123", // Optional: for attribution
  meta: { campaign: "twitter" }, // Optional: custom metadata
});

// Create share URL
const shareUrl = `https://example.com/article/crypto-guide?token=${token}`;

Validating Tokens

app.post("/api/unlock", async (req) => {
  const { token, wrappedDek, publicKey, articleId } = req.body;

  // Validate the token
  const validation = tokens.validate(token);
  if (!validation.valid) {
    return res.status(401).json({ error: validation.message });
  }

  // Log for analytics
  console.log("Unlock via share link", {
    tokenId: validation.payload.tid,
    tier: validation.payload.tier,
    articleId,
  });

  // Optional: check usage count from Redis/DB
  // if (validation.payload.maxUses) {
  //   const uses = await redis.incr(`token:${validation.payload.tid}:uses`);
  //   if (uses > validation.payload.maxUses) {
  //     return res.status(403).json({ error: "Token usage limit exceeded" });
  //   }
  // }

  // Unlock using the token
  const result = server.unlockWithToken(
    validation.payload,
    wrappedDek,
    publicKey,
    articleId,
  );

  return res.json({ ...result, tokenId: validation.payload.tid });
});

Token Structure

Tokens are URL-safe, signed payloads:

interface UnlockTokenPayload {
  v: 1; // Version
  tid: string; // Unique token ID (for tracking/revocation)
  tier: string; // Tier this grants access to
  articleId?: string; // Specific article (if restricted)
  userId?: string; // Creator attribution
  maxUses?: number; // Usage limit
  iat: number; // Issued at (Unix timestamp)
  exp: number; // Expires at (Unix timestamp)
  meta?: Record<string, any>; // Custom metadata
}

Full Example: Share Link Flow

// 1. Publisher generates share link
app.post("/api/share", async (req, res) => {
  const { tier, articleId, expiresIn, maxUses } = req.body;

  const token = tokens.generate({ tier, articleId, expiresIn, maxUses });
  const payload = tokens.peek(token);

  res.json({
    token,
    tokenId: payload.tid,
    shareUrl: `https://example.com/article/${articleId}?token=${token}`,
    expiresAt: new Date(payload.exp * 1000).toISOString(),
  });
});

// 2. Reader clicks link, client unlocks
// (Client extracts token from URL, sends with unlock request)

// 3. Server validates and unlocks
app.post("/api/unlock", async (req, res) => {
  const { token, wrappedDek, publicKey, articleId } = req.body;

  if (token) {
    const validation = tokens.validate(token);
    if (!validation.valid) {
      return res.status(401).json({ error: validation.message });
    }

    // Full audit trail
    await analytics.log("share_link_unlock", {
      tokenId: validation.payload.tid,
      tier: validation.payload.tier,
      articleId,
      ip: req.ip,
    });

    const result = server.unlockWithToken(
      validation.payload,
      wrappedDek,
      publicKey,
    );
    return res.json(result);
  }

  // Regular unlock flow...
});

How It Works

Envelope Encryption

Capsule uses envelope encryption for efficient multi-recipient encryption:

Content → [AES-256-GCM] → Encrypted Content
              ↓
           DEK (unique per article)
              ↓
    ┌─────────┼─────────┐
    ↓         ↓         ↓
  Key #1    Key #2    Key #3
    ↓         ↓         ↓
 Wrapped   Wrapped   Wrapped
 DEK #1    DEK #2    DEK #3
  • Content is encrypted ONCE with a unique DEK (Data Encryption Key)
  • The DEK is wrapped with MULTIPLE key-wrapping keys
  • Different users can unlock using different wrapped keys
  • No need to re-encrypt content when adding access paths

Time-Bucket Keys (TOTP)

When using TotpKeyProvider, keys rotate automatically:

  • Keys are derived from masterSecret + keyId + bucketId using HKDF
  • Bucket ID changes every bucketPeriodSeconds (default: 30s)
  • Provider returns current AND next bucket (handles clock drift)
  • When bucket expires, old wrapped keys become invalid (forward secrecy)

Framework Examples

Next.js

// lib/capsule.ts
import { createCmsServer, createTotpKeyProvider } from "@sesamy/capsule-server";

const totp = createTotpKeyProvider({
  masterSecret: process.env.MASTER_SECRET!,
});

export const cms = createCmsServer({
  getKeys: (keyIds) => totp.getKeys(keyIds),
});

// app/article/[slug]/page.tsx
export default async function ArticlePage({ params }) {
  const article = await getArticle(params.slug);

  const encryptedHtml = await cms.encrypt(article.id, article.premiumContent, {
    keyIds: ["premium"],
    format: "html",
  });

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.preview}</div>
      <div dangerouslySetInnerHTML={{ __html: encryptedHtml }} />
    </article>
  );
}

Astro

---
// src/pages/article/[slug].astro
import { createCmsServer, createTotpKeyProvider } from '@sesamy/capsule-server';

const totp = createTotpKeyProvider({
  masterSecret: import.meta.env.MASTER_SECRET,
});

const cms = createCmsServer({
  getKeys: (keyIds) => totp.getKeys(keyIds),
});

const article = await getArticle(Astro.params.slug);
const { attribute } = await cms.encryptForTemplate(
  article.id,
  article.premiumContent,
  { keyIds: ['premium'] }
);
---
<article>
  <h1>{article.title}</h1>
  <div set:html={article.preview} />
  <div
    data-capsule={attribute}
    data-capsule-id={article.id}
  >
    <p>Subscribe to unlock...</p>
  </div>
</article>

Express

import express from "express";
import {
  createCmsServer,
  createTotpKeyProvider,
  createSubscriptionServer,
} from "@sesamy/capsule-server";

const app = express();

const totp = createTotpKeyProvider({
  masterSecret: process.env.MASTER_SECRET!,
});
const cms = createCmsServer({ getKeys: (keyIds) => totp.getKeys(keyIds) });
const server = createSubscriptionServer({
  masterSecret: process.env.MASTER_SECRET!,
});

// Encrypt content
app.get("/api/article/:id", async (req, res) => {
  const article = await db.getArticle(req.params.id);
  const encrypted = await cms.encrypt(article.id, article.content, {
    keyIds: ["premium"],
  });
  res.json({ ...article, encrypted });
});

// Unlock endpoint
app.post("/api/unlock", async (req, res) => {
  // Validate user subscription first!
  const { keyId, wrappedDek, publicKey } = req.body;
  const result = await server.unlockForUser({ keyId, wrappedDek }, publicKey);
  res.json(result);
});

Security Notes

  • Master secret: Store in KMS (AWS Secrets Manager, HashiCorp Vault, etc.)
  • Bucket period: Determines maximum revocation delay (shorter = faster revocation, more wrapped keys)
  • Per-article keys: Are static (no automatic revocation) - use for permanent purchases
  • Key isolation: CMS only needs key IDs, not the master secret (if using external key provider)
  • User validation: Always validate subscription before calling unlockForUser()

API Reference

CmsServer

import { createCmsServer, CmsServer } from '@sesamy/capsule-server';

const cms = createCmsServer(options: CmsServerOptions);

interface CmsServerOptions {
  getKeys: (keyIds: string[]) => Promise<KeyEntry[]>;  // Required
  logger?: (msg: string, level: 'info' | 'warn' | 'error') => void;
}

interface KeyEntry {
  keyId: string;              // Key identifier
  key: Buffer | string;       // 256-bit AES key
  expiresAt?: Date | string;  // Optional expiration
}

// Encrypt content
cms.encrypt(articleId, content, { keyIds, format?, ... }): Promise<EncryptedArticle | string>;

// Get all formats for templates
cms.encryptForTemplate(articleId, content, { keyIds }): Promise<{ data, json, attribute, html }>;

TotpKeyProvider

import { createTotpKeyProvider, TotpKeyProvider } from '@sesamy/capsule-server';

const totp = createTotpKeyProvider(options: TotpKeyProviderOptions);

interface TotpKeyProviderOptions {
  masterSecret: Buffer | string;   // Required
  bucketPeriodSeconds?: number;    // Default: 30
}

// Get time-bucket keys (current + next for each keyId)
totp.getKeys(keyIds: string[]): Promise<KeyEntry[]>;

// Get static article key
totp.getArticleKey(articleId: string): Promise<KeyEntry>;

SubscriptionServer

import { createSubscriptionServer, SubscriptionServer } from '@sesamy/capsule-server';

const server = createSubscriptionServer(options: SubscriptionServerOptions);

interface SubscriptionServerOptions {
  masterSecret: string | Buffer;   // Required
  bucketPeriodSeconds?: number;    // Default: 30
}

// For CMS key fetching (if not using TOTP locally)
server.getBucketKeysResponse(keyId: string): BucketKeysResponse;

// For user unlock
server.unlockForUser(
  wrappedKey: { keyId, wrappedDek },
  userPublicKey: string,
  staticKeyLookup?: (keyId: string) => Buffer | null
): Promise<UnlockResponse>;

// For token-based unlock (share links)
server.unlockWithToken(
  tokenPayload: UnlockTokenPayload,
  wrappedDekB64: string,
  userPublicKey: string,
  articleId?: string
): UnlockResponse;

TokenManager

import { createTokenManager, TokenManager } from '@sesamy/capsule-server';

const tokens = createTokenManager(options: TokenManagerOptions);

interface TokenManagerOptions {
  secret: string | Buffer;  // Required, min 32 bytes recommended
}

// Generate a signed token
tokens.generate(options: GenerateTokenOptions): string;

interface GenerateTokenOptions {
  tier: string;              // Required: tier to grant access to
  expiresIn: string | number; // Required: "1h", "24h", "7d", or seconds
  articleId?: string;         // Optional: restrict to article
  maxUses?: number;           // Optional: usage limit
  userId?: string;            // Optional: creator attribution
  meta?: Record<string, any>; // Optional: custom metadata
}

// Validate a token
tokens.validate(token: string): TokenValidationResult | TokenValidationError;

// Peek at payload without validating (for logging)
tokens.peek(token: string): UnlockTokenPayload | null;

See TypeScript definitions for full type documentation.