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

v0.12.0

Published

Client-side decryption library for Capsule - Secure article unlocking using Web Crypto API

Readme

@sesamy/capsule

Browser-side DCA (Delegated Content Access) client — parse encrypted pages, call issuer unlock endpoints, and decrypt content.

Installation

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

Quick Start

The simplest integration — auto-detects the issuer and share token, then decrypts and renders:

import { DcaClient, hasDcaContent } from "@sesamy/capsule";

if (hasDcaContent()) {
  const client = new DcaClient();
  const content = await client.processPage();
  client.renderToPage(content);
}

Step-by-Step

For more control, use the individual methods:

import { DcaClient } from "@sesamy/capsule";

const client = new DcaClient();

// Parse the DCA manifest from the current page
const page = client.parsePage();

// Unlock via an issuer
const keys = await client.unlock(page, "sesamy");

// Decrypt a specific content item
const html = await client.decrypt(page, "bodytext", keys);

// Inject into the DOM
document.querySelector('[data-dca-content-name="bodytext"]')!.innerHTML = html;

Decrypt All Content

const page = client.parsePage();
const keys = await client.unlock(page, "sesamy");
const decrypted = await client.decryptAll(page, keys);

for (const [contentName, html] of Object.entries(decrypted)) {
  document.querySelector(`[data-dca-content-name="${contentName}"]`)!.innerHTML = html;
}

Share Links

import { parseShareToken } from "@sesamy/capsule";

// Standalone function — reads ?share= from current URL
const shareToken = parseShareToken();

// Or via the static method
const shareToken = DcaClient.getShareTokenFromUrl();

if (shareToken) {
  const keys = await client.unlockWithShareToken(page, "sesamy", shareToken);
  const html = await client.decrypt(page, "bodytext", keys);
}

JSON API (non-DOM)

const res = await fetch("/api/article/123");
const json = await res.json();

const page = client.parseJsonResponse(json);
const keys = await client.unlock(page, "sesamy");
const content = await client.decrypt(page, "bodytext", keys);

API Reference

new DcaClient(options?)

| Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | fetch | typeof fetch | globalThis.fetch | Custom fetch function (e.g. for adding auth headers) | | unlockFn | (url, body) => Promise<DcaUnlockResponse> | — | Custom unlock function (replaces fetch-based unlock) | | wrapKeyCache | DcaWrapKeyCache \| false | IndexedDB-backed | Key-value cache for wrapKey reuse across pages. Pass false to disable, or supply a custom cache. | | clientBound | boolean | false | Enable client-bound transport (RSA-OAEP key wrapping) | | rsaKeySize | 2048 \| 4096 | 2048 | RSA key size for client-bound transport | | keyDbName | string | "dca-keys" | IndexedDB database name for key pair storage |

client.parsePage(root?)

Parses the DCA manifest from the DOM. Looks for a single <script class="dca-manifest"> element.

const page = client.parsePage(); // defaults to document
const page = client.parsePage(someElement); // scoped parse

Returns a DcaParsedPage used by other methods.

client.parseJsonResponse(json)

Parses a JSON API response into a DcaParsedPage.

const page = client.parseJsonResponse(apiResponse);

client.unlock(page, issuerName, additionalBody?)

Calls the issuer's unlock endpoint and returns keys.

| Param | Type | Description | | ----- | ---- | ----------- | | page | DcaParsedPage | Parsed page data | | issuerName | string | Which issuer to call | | additionalBody | Record<string, unknown> | Extra fields in the request body (e.g. auth tokens) |

const keys = await client.unlock(page, "sesamy", {
  authorization: "Bearer " + userToken,
});

client.unlockWithShareToken(page, issuerName, shareToken, additionalBody?)

Calls the unlock endpoint with a share token attached.

client.decrypt(page, contentName, unlockResponse)

Decrypts a single content item using the keys from unlock().

Handles both delivery modes automatically:

  • direct — decrypts with the returned contentKey
  • wrapKey — unwraps the contentKey from manifest.content[name].wrappedContentKey first, then decrypts

Also handles client-bound transport (RSA-OAEP unwrapping) and caches wrapKeys when a wrapKeyCache is configured (enabled by default).

client.decryptAll(page, unlockResponse)

Decrypts all content items in the unlock response.

const all = await client.decryptAll(page, keys);
// { bodytext: "<p>...</p>", sidebar: "<aside>...</aside>" }

client.processPage(options?)

Convenience method: parse → unlock → decryptAll in one call. Auto-detects the issuer (first key in manifest.issuers) and share token (from ?share= URL parameter).

const content = await client.processPage();

// With explicit options
const content = await client.processPage({
  issuerName: "sesamy",          // override auto-detected issuer
  shareToken: null,              // skip share token detection
  root: someElement,             // scope DOM parsing
  additionalBody: { auth: "…" }, // extra fields for unlock request
});

client.renderToPage(content, root?)

Injects decrypted content into the DOM. Finds elements with matching data-dca-content-name attributes and sets their innerHTML. Returns the set of content names that were rendered.

const content = await client.processPage();
const rendered = client.renderToPage(content);
// rendered: Set { "bodytext", "sidebar" }

DcaClient.hasDcaContent(root?)

Static method. Returns true if the page contains a <script class="dca-manifest"> element. Also available as a standalone import:

import { hasDcaContent } from "@sesamy/capsule";

if (hasDcaContent()) { /* page has DCA content */ }

DcaClient.getShareTokenFromUrl(paramName?)

Static method. Extracts a share token from the current URL query params.

const token = DcaClient.getShareTokenFromUrl(); // reads ?share=...
const token = DcaClient.getShareTokenFromUrl("token"); // reads ?token=...

Returns null in non-browser environments or when the param is absent.

client.getPublicKey()

Returns the client's RSA-OAEP public key as base64url-encoded SPKI. Generates and persists a key pair to IndexedDB on first call. Only needed when clientBound: true.

client.hasKeyPair()

Checks if an RSA key pair already exists in IndexedDB.

Wrap Key Cache

When the issuer returns wrapKeys (instead of direct contentKeys), the client caches them so the next page navigation can decrypt without another unlock call. An IndexedDB-backed cache is enabled by default.

// Default: IndexedDB cache, no configuration needed
const client = new DcaClient();

// Custom cache (e.g. sessionStorage)
const cache: DcaWrapKeyCache = {
  async get(key) { return sessionStorage.getItem(key); },
  async set(key, value) { sessionStorage.setItem(key, value); },
};
const client = new DcaClient({ wrapKeyCache: cache });

// Disable caching
const client = new DcaClient({ wrapKeyCache: false });

Cache keys use the format dca:wk:{scope}:{kid} — scoped by scope and rotation version (kid), so wrapKeys from one unlock are reusable across every article in the same scope and rotation.

Client-Bound Transport

When clientBound: true, the client generates an RSA-OAEP key pair stored in IndexedDB (private key is non-extractable). Keys returned by the issuer are wrapped with the client's public key, ensuring they can only be decrypted on the same device.

const client = new DcaClient({
  clientBound: true,
  rsaKeySize: 2048,
});

Request Format

The unlock request sends resourceJWT and keys (copied verbatim from manifest.issuers[issuerName].keys). Each entry carries a scope field that is AAD-bound — the issuer uses it directly for access scope resolution.

Types

import type {
  DcaClientOptions,
  DcaParsedPage,
  DcaWrapKeyCache,
  DcaManifest,
  DcaUnlockResponse,
  DcaProcessPageOptions,
} from "@sesamy/capsule";