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

@growth-labs/video

v0.4.11

Published

Astro 6 integration for HLS video playback from R2, reusable R2 HLS bundle ingestion, captions/chapters sidecars, YouTube embeds, and Worker-proxied premium gating from a distinct private R2 origin.

Readme

@growth-labs/video

Astro 6 integration for HLS video playback from R2, reusable R2 HLS bundle ingestion, captions/chapters sidecars, YouTube embeds, and Worker-proxied premium gating from a distinct private R2 origin.

Install

pnpm add @growth-labs/video vidstack

Config

import video from '@growth-labs/video'

video({
  publicDomain: 'media.example.com',
  r2Binding: 'MEDIA_BUCKET',
  admin: {
    importRoute: {
      enabled: true,
      path: '/api/admin/video/import',
      requireAuth: true,
    },
  },
})

Premium playback additionally needs premium.enabled, a distinct private R2 binding, an accessCheckModule (a module-spec pointing at a file in your own codebase), and a GL_VIDEO_SESSION_SECRET secret of at least 32 bytes:

// astro.config.mjs
video({
  publicDomain: 'media.example.com',
  r2Binding: 'MEDIA_BUCKET',          // public images and public video only
  premium: {
    enabled: true,
    r2Binding: 'PREMIUM_MEDIA',       // private bucket; no r2.dev or custom domain
    sessionScope: 'per-video',  // default; 'wildcard' opts into the 0.2.x behavior
    sessionTtl: '1h',           // duration string: '1h' | '30m' | '2h30m' | '45s'
    accessCheckModule: { module: '/src/lib/video-access-check', export: 'check' },
  },
})

premium.r2Binding is required when premium.enabled=true and must differ from the public r2Binding. The package rejects a shared binding at build time. A different binding name is necessary but not sufficient: the consumer must bind it to a genuinely separate bucket with both r2.dev and custom-domain access disabled. The package reads protected objects only through the Worker binding; the signed per-video session cookie is resource-bound and expires. Every playlist and segment request re-runs the configured access check and requires its current subjectId to match the subject bound into the signed session. Revocation, logout, account deletion, or replay under another identity therefore fails before any private R2 object read. Session payloads are strict: issuer and verifier share the same bounded payload policy, unexpected signed payload keys are rejected, and oversized issuer payloads fail before signing. Malformed session cookies fail closed as the same protected 403 response.

For a controlled migration or incident recovery where an existing premium catalog still lives in the public bucket, set premium.r2Binding to the public binding and explicitly add allowPublicStorage: true. The authenticated route still enforces the signed session, current access check, matching subject, and private/no-store response policy before every R2 read. This is not private storage: direct public object URLs remain fetchable. The option defaults to false; remove it after copying the catalog to a distinct private bucket.

Per-subject placement selection

Consumers migrating rows between public and private storage can configure both bindings without duplicating playback routes:

premium: {
  enabled: true,
  r2Binding: 'PUBLIC_MEDIA',
  allowPublicStorage: true,
  placementBindings: {
    public: 'PUBLIC_MEDIA',
    private: 'PREMIUM_MEDIA',
  },
  accessCheckModule: { module: '/src/lib/video-access-check', export: 'check' },
}

The consumer's access check resolves verified placement by stable subject ID:

return {
  allowed: true,
  subjectId: user.id,
  playbackPlacement: placement
    ? { verified: true, storageClass: placement.storageClass }
    : { verified: false },
}

The package never infers placement from channel names, slugs, routes, or video IDs. A verified class is signed into the session and must match the fresh access result on every manifest, segment, and track request. Unverified, mismatched, or unconfigured placement fails with a protected terminal error before R2 access; there is no private-to-public fallback. Omit playbackPlacement entirely only for the legacy premium.r2Binding path.

[[r2_buckets]]
binding = "MEDIA_BUCKET"
bucket_name = "<site>-media"

[[r2_buckets]]
binding = "PREMIUM_MEDIA"
bucket_name = "<site>-premium-media"

Provisioning buckets, bindings, domains, and the Secrets Store entry is an operator-owned deployment step. This package release does not mutate those surfaces.

// /src/lib/video-access-check.ts
import type { AccessCheck } from '@growth-labs/video'

export const check: AccessCheck = async ({ videoId, request, env }, context) => {
  // Returns { allowed, reason?, subjectId? }
}

Why a module spec instead of an inline function? The integration runs in the build-time Node process; the session route runs in the Worker runtime — a different process. An inline accessCheck registered at build time never reaches the Worker bundle, so every request would 500 with access_check_not_configured. The module-spec lets the Vite plugin emit a real static ES import at the top of the virtual:growth-labs/video/access-check module, so the function is bundled with the worker. Matches the same pattern as @growth-labs/auth's renderers (see auth 0.3.7+ MIGRATION.md).

per-video scope (the 0.3.0 default) issues a cookie restricted to one videoId. Both scopes re-check current access and subject identity for every playlist and segment request; wildcard only broadens which video IDs the signed session can address. Keep sessionTtl short enough to limit stolen-cookie lifetime even though replay without the bound authenticated identity is rejected.

Customizing the layout

Since 0.3.5, <Video> exposes a named slot layout for replacing the Vidstack chrome (controls, sliders, menus) without losing anything the package wires up around the player — premium bootstrap, accessCheck, gl:video-* analytics events, captions/chapter <track> elements, progress marks.

---
import { Video } from '@growth-labs/video/components'
import MyVideoLayout from '../components/MyVideoLayout.astro'
---

<Video videoId="..." title="..." premium>
  <MyVideoLayout slot="layout" />
</Video>

If you omit the slot, you get Vidstack's stock <media-video-layout> (the package's current default). If you provide it, your slotted content replaces the default entirely — it is rendered as a direct child of <media-player>, so any Vidstack layout primitive (<media-controls>, <media-time-slider>, <media-quality-radio-group>, etc.) works.

Consumers writing a custom layout typically need to import Vidstack's full UI element registry once at the top of their component (Vidstack's vidstack/elements only defines a minimal set; the full primitive set is under vidstack/player/ui):

// inside MyVideoLayout.astro's <script>
import 'vidstack/player/ui'

Captions and chapter <track> elements continue to be rendered by <Video> inside <media-provider> — do not duplicate them inside your slotted layout.

R2 Layout

video/<videoId>/hls/master.m3u8
video/<videoId>/hls/<rendition>/index.m3u8
video/<videoId>/hls/<rendition>/init.mp4
video/<videoId>/hls/<rendition>/segment-00001.m4s
video/<videoId>/poster.jpg
video/<videoId>/text/captions_en.vtt
video/<videoId>/text/chapters.vtt

Ingest Helpers

import { ingestHlsBundle, writeCaptionsVtt, writeChaptersVtt } from '@growth-labs/video/utils'

await ingestHlsBundle({
  bucket: env.MEDIA_BUCKET,
  publicDomain: 'media.example.com',
  videoId: 'intro-video',
  files: [
    { path: 'master.m3u8', body: masterManifest },
    { path: 'h264/720p/index.m3u8', body: renditionManifest },
    { path: 'h264/720p/init.mp4', body: initBytes },
    { path: 'h264/720p/segment-00001.m4s', body: segmentBytes },
  ],
  poster: { path: 'poster.jpg', body: posterBytes, contentType: 'image/jpeg' },
})

await writeCaptionsVtt({ bucket: env.MEDIA_BUCKET, videoId: 'intro-video', vtt: captionsVtt })
await writeChaptersVtt({ bucket: env.MEDIA_BUCKET, videoId: 'intro-video', vtt: chaptersVtt })

The ingestion helper is the public-bundle path: it validates master.m3u8, referenced rendition playlists, segment paths, and local HLS URI="..." attributes such as EXT-X-MAP, EXT-X-MEDIA, EXT-X-I-FRAME-STREAM-INF, and EXT-X-KEY before writing to R2 and returning a public manifest URL. Protected bundles are written by the owner publishing pipeline to premium.r2Binding; never persist or expose a public origin URL for them. Absolute/external URLs and unsafe relative paths are rejected. Transcoding is out of scope; pass already-generated HLS and VTT files.

All R2 key helpers normalize videoId with the same safe contract: IDs must start with an ASCII letter or number and then contain only ASCII letters, numbers, _, or -. Text-track helpers write only under video/<videoId>/text/captions_<lang>.vtt and video/<videoId>/text/chapters.vtt.

Injected routes read bindings from cloudflare:workers env; no locals.runtime shim is required.

Premium access verification

The repository-level verifier proves the deployed route and legacy origin in a single redacted run. PREMIUM_HLS_TEST_FIXTURE contains an absolute path to a regular JSON file with mode 0600; it never contains the fixture value itself.

{
  "masterUrl": "https://site.example/api/video/playback/<resource>/hls/master.m3u8",
  "authorizedCookie": "gl_video_session=<owner-provisioned-authorized-cookie>",
  "expiredCookie": "gl_video_session=<owner-provisioned-expired-cookie>",
  "wrongAudienceCookie": "gl_video_session=<owner-provisioned-wrong-resource-cookie>",
  "nonEntitledCookie": "fronts_session=<owner-provisioned-non-entitled-session>",
  "publicOriginMasterUrl": "https://media.example/video/<resource>/hls/master.m3u8",
  "publicOriginSegmentUrl": "https://media.example/video/<resource>/hls/<rendition>/<segment>"
}
pnpm run hls:access:verify -- --fixture-env PREMIUM_HLS_TEST_FIXTURE

The verifier requires 403 for anonymous, expired, wrong-audience, and non-entitled gateway requests, 200 for authorized playlists/segments, 206 for an authorized range, and 403 or 404 for the corresponding unsigned legacy-origin objects. It follows no redirects, rejects cross-origin and path-escaping manifest references, bounds all manifest reads, and prints only named checks and status codes—never URLs, paths, cookies, manifests, or media bytes.