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

streamlock

v0.1.0

Published

Runtime-agnostic subscription gating for HLS media. Rewrites AES-128 m3u8 key URIs, mints short-lived session tokens, and enforces per-episode entitlements on Cloudflare Workers, AWS Lambda@Edge, Google Cloud, or any Node 18+ runtime.

Readme

streamlock

English | 日本語

Runtime-agnostic subscription gating for HLS media.

streamlock is the extraction of the paywall that sits in front of koelink's encrypted audio/video streams. It decides who may fetch an HLS manifest, who may fetch the AES-128 decryption key, and rewrites the manifest so the player asks your key endpoint instead of the raw enc.key.

The core has zero runtime dependencies and only uses Web-standard primitives (Request/Response/URL/WebCrypto/fetch), so the same handler runs on:

  • Cloudflare Workers (R2 + KV + Cache API)
  • AWS (CloudFront + Lambda@Edge + S3 + DynamoDB)
  • Google Cloud (Cloud Run / Cloud Functions + Cloud Storage + Firestore)
  • Any Node 18+ server (ECS, Fargate, a VM, Fastly Compute, Deno, Bun)

In production, streamlock powers the media delivery for the voice-actor radio platform https://koelink.co.jp (こえリンク) — every premium episode's manifest, key and segment requests go through this gate.


How it works

                 ┌──────────────────────────────────────────────┐
  player ──────▶ │  streamlock (Worker / Lambda@Edge / Node)    │
                 │                                              │
  1. GET master.m3u8 ──▶ verify ID token ──▶ entitlement.check │
                 │        │ allowed?                             │
                 │        ▼                                     │
                 │   rewrite #EXT-X-KEY URI ──▶ /get-key?token= │
                 │                                              │
  2. GET /get-key?token= ──▶ verify HMAC session token         │
                 │        ──▶ read _private_keys/.../enc.key    │
                 │                                              │
  3. GET seg1.ts ──▶ (optional auth) ──▶ object storage ──▶ CDN │
                 └──────────────────────────────────────────────┘
                        │                        │
                   entitlement store        object storage
                   (KV / DynamoDB)          (R2 / S3)
  1. Manifest — for premium episodes the gate verifies the caller's ID token, asks the entitlement adapter whether the user subscribes to the program, and rewrites every #EXT-X-KEY URI to …/get-key?token=<short-lived HMAC JWT>. The token only authorizes the one key object ({ objectKey }).
  2. Key — /get-key verifies the HMAC token (no ID token needed; this is the one endpoint iOS AVFoundation calls without Origin/Referer) and streams the AES-128 key. Keys are no-store.
  3. Segments — .ts/.vtt are served from object storage with long Cache-Control. Because the CDN caches by path (not by token), the gate runs roughly once per object per edge location, not once per viewer.

Gating happens on the manifest and the key. Segments themselves are encrypted, so a leaked segment URL is useless without the key. You can turn per-segment entitlement checks off with checkAuthOnSegments: false.


Install

npm install streamlock

AWS adapters additionally need the AWS SDK (peer dependencies):

npm install @aws-sdk/client-s3 @aws-sdk/lib-dynamodb

Quick start

Cloudflare Workers

import { createMediaGate, createFirebaseVerifier } from "streamlock";
import { createCloudflareAdapters, createCloudflareCache } from "streamlock/adapters/cloudflare";

export default {
  async fetch(request, env, ctx) {
    const { storage, entitlement } = createCloudflareAdapters(env);
    const gate = createMediaGate({
      basePath: "/koelink/api",
      allowedOrigins: [env.ALLOWED_ORIGIN],
      storage,
      entitlement,
      verifyToken: createFirebaseVerifier({ projectId: env.FIREBASE_PROJECT_ID }),
      hmacSecret: () => env.HMAC_SECRET,
      cache: createCloudflareCache(),
    });
    return gate.handleRequest(request, { waitUntil: (fn) => ctx.waitUntil(fn) });
  },
};

Bindings: R2 MEDIA_BUCKET, KV premium and role. See examples/cloudflare-worker.js.

AWS Lambda@Edge (CloudFront + S3 + DynamoDB)

import { createMediaGate, createFirebaseVerifier } from "streamlock";
import { createAwsAdapters } from "streamlock/adapters/aws";
import { S3Client } from "@aws-sdk/client-s3";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

const s3 = new S3Client({ region: "ap-northeast-1" });
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "ap-northeast-1" }));

const gate = createMediaGate({
  basePath: "/koelink/api",
  allowedOrigins: ["https://koelink.co.jp"],
  ...createAwsAdapters({ s3, bucket: "media", docClient, tableName: "entitlements" }),
  verifyToken: createFirebaseVerifier({ projectId: "my-project" }),
  hmacSecret: () => process.env.HMAC_SECRET,
});

examples/aws-lambda-edge.js includes the CloudFront event ⇄ Request/Response conversion. Build with esbuild:

npx esbuild examples/aws-lambda-edge.js --bundle --platform=node --target=node18 \
  --outfile=dist/lambda.js

Lambda@Edge caveats: no environment variables (bake config at build time, see examples/aws-config.js), 1 MB unpacked bundle limit, and responses must be fully materialized. Keep the small requests (m3u8, get-key) on the function and let CloudFront cache the segments.

Google Cloud (Cloud Run / Cloud Functions + GCS + Firestore)

import { Storage } from "@google-cloud/storage";
import { Firestore } from "@google-cloud/firestore";
import { createMediaGate, createFirebaseVerifier } from "streamlock";
import { createGcpAdapters } from "streamlock/adapters/gcp";

const gate = createMediaGate({
  basePath: "/koelink/api",
  allowedOrigins: ["https://koelink.co.jp"],
  ...createGcpAdapters({
    bucket: new Storage().bucket(process.env.MEDIA_BUCKET),
    firestore: new Firestore(),
  }),
  verifyToken: createFirebaseVerifier({ projectId: process.env.FIREBASE_PROJECT_ID }),
  hmacSecret: () => process.env.HMAC_SECRET,
});

examples/gcp-cloud-run.js is a deployable Node server (PORT=8080) and exports a nodeAdapter(req, res) you can wrap with firebase-functions/v2/https onRequest. GCS metadata + Cloud CDN cache the segments; Cloud CDN keys on the path (not the token), so cached bytes are shared across subscribers.

Any Node 18+ server

examples/express.js runs a demo with in-memory adapters:

node examples/express.js

API

createMediaGate(options)

Returns { handleRequest(request, ctx?) }. ctx may contain waitUntil(fn) for runtimes that support background tasks.

| Option | Default | Purpose | | --- | --- | --- | | storage | — | { get(key) => Promise<StoredObject\|null> } | | entitlement | — | { check({ userId, programId, claims }) => Promise<boolean> } | | verifyToken | — | (token) => Promise<claims\|null> | | hmacSecret | — | string / () => Promise<string> for session tokens | | basePath | /api | URL prefix | | allowedOrigins | [] | Origin/Referer allow-list; empty = open | | sessionTokenTtlSeconds | 7200 | Key token lifetime | | keyPrefix | _private_keys | Storage prefix for enc.key | | episodePolicy | suffix -b/-c | { isPremium(episodeId) } | | checkAuthOnSegments | true | Also gate .ts/.vtt | | cache | — | { match, put } for segment caching | | authCacheTtlSeconds | 300 | Entitlement memo TTL |

Auth

createJwksVerifier({ jwksUri, audience, issuer }) verifies any RS256 JWKS issuer (Cognito, Auth0, …). createFirebaseVerifier({ projectId }) is a convenience wrapper for Firebase Authentication.

Storage adapter contract

get(key: string): Promise<{
  text(): Promise<string>;      // manifests
  body: ReadableStream | null;  // segments / keys
  contentType: string | null;
  etag: string | null;
  size: number | null;
} | null>

Entitlement adapter contract

check({ userId, programId, claims }): Promise<boolean>

The bundled adapters follow this entitlement convention:

  • Cloudflare: KV premium:<programId>:<userId> = "1", or KV admin:<userId> / owner:<programId>:<userId>.
  • AWS: DynamoDB keys premium:<programId>:<userId>, admin:<userId>, owner:<programId>:<userId>.
  • Google Cloud: ID-token claim role ("admin" / programId / array), or Firestore users/{userId}/premiums/{programId} with status in active|trialing and a valid currentPeriodEnd.

Security notes

  • Session tokens are HS256, short-lived, and scoped to a single objectKey.
  • The segment cache key excludes the token, so cached bytes are shared across subscribers. This is what keeps edge cost flat.
  • /get-key intentionally skips the Origin/Referer check (iOS AVFoundation does not send them); it is protected by the signed token instead.
  • Entitlement results are memoized in-process for authCacheTtlSeconds. Tune it to how quickly revoked subscriptions must take effect.
  • Revocation: keep key tokens short (60–300 s) and re-check the manifest on each playlist refresh.

How it compares

| | streamlock | Cloudflare Stream / Mux | Castopod / podcast hosts | firestore-stripe-payments | | --- | --- | --- | --- | --- | | Bring your own storage (R2/S3/GCS) | ✅ | ❌ | varies | n/a | | Per-program subscription gating | ✅ | limited | ✅ (premium feeds) | ✅ | | Runs on CF and AWS and GCP | ✅ | ❌ | ❌ | Firebase only | | No egress lock-in | ✅ | ❌ | varies | n/a | | HLS AES-128 key delivery | ✅ | managed | ❌ | n/a |

The differentiator is self-hosted, multi-cloud, per-episode gating with no egress fees — the paid-podcast/audio niche usually forces you onto a managed platform or a platform-specific lock.


License

Licensed under the Server Side Public License v1 (SSPL-1.0) (LICENSE).

SSPL is the license used by MongoDB. It grants the usual source-available rights (use, copy, modify, redistribute), and Section 13 adds a strong reciprocal requirement: anyone who offers the functionality of the software to third parties as a service must make the complete Service Source Code (their modifications plus the management/hosting software they use) available to everyone at no charge under SSPL.

Concretely, this means:

  • Self-hosting (your own servers, your own listeners) — fine.
  • Running it internally for your own product/company — fine.
  • Offering it as a hosted/managed service to third parties — you must publish the full corresponding source for that service under SSPL.

This is deliberately stronger than a permissive license: it protects the licensor's managed-service offering while keeping the software open and self-hostable. SSPL-1.0 is not OSI-approved, which some organizations decline to adopt — verify that this fits your consumers before relying on it.

Commercial licensing for managed-service providers is available from the licensor.


Status

0.1.0 — extracted from the koelink production media server. Core + Cloudflare, AWS, Google Cloud and memory adapters, examples and tests are in place.