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

dp1-js

v2.4.1

Published

Node.js SDK for the DP-1 protocol, ported from dp1-go with a TypeScript stack.

Readme

dp1-js

Lint Test

Node.js SDK for the DP-1 protocol, kept intentionally dependency-light.

Overview

dp1-js provides parsing, validation, canonicalization, hashing, and signing helpers for DP-1 playlists, playlist groups, ref manifests, and Feral File channel documents.

It is designed for Node.js 22+ and ships dual ESM/CJS entrypoints through the package root. Schema validation is precompiled at package build time, so it also runs on Node-compatible runtimes that forbid dynamic code generation — Cloudflare Workers being the tested one (see Edge runtimes).

Features

  • Parse and validate DP-1 playlist, ref manifest, and channel documents (plus deprecated playlist-group, see below).
  • Schema-validate unsigned drafts via Validate* helpers (requireSignatures: false).
  • Build DP-1 documents and leaf structures with fluent builders backed by AJV schemas, precompiled so no schema is compiled at runtime.
  • Canonicalize signing payloads using RFC 8785-style JSON canonicalization.
  • Compute and verify payload hashes and signatures (Ed25519, and EIP-191 wallet signatures).
  • Merge display preferences with DP-1 resolution order.
  • Resolve playlist displayAt schedules into an active playback set and next timer.

Install

npm install dp1-js

Quick Start

Build a playlist and validate unsigned

import { PlaylistBuilder, PlaylistItemBuilder, NoteBuilder } from 'dp1-js';

const playlist = new PlaylistBuilder()
  .title('Draft show')
  .addItem(new PlaylistItemBuilder().source('https://example.com/a.html'))
  .note(new NoteBuilder().text('Intermission').durationSeconds(20))
  .build();

build() schema-validates an unsigned document (requireSignatures: false). Required fields still must be set — omitting title / channel slug fails AJV. When omitted, document builders generate stable id and created on first build() (persisted on the builder). Document builders also cover playlist groups, channels, and ref manifests.

format: uri follows AJV/ajv-formats (absolute URIs, including non-http(s) schemes). Runtime fetch policies (for example dynamicQuery) may still reject non-HTTP endpoints.

Parse and validate a playlist

import { ParseAndValidatePlaylist } from 'dp1-js';

const rawPlaylist = JSON.stringify({
  dpVersion: '1.1.0',
  title: 'Example Playlist',
  items: [
    {
      source: 'https://example.com/artwork.html',
    },
  ],
  signatures: [
    {
      alg: 'ed25519',
      kid: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK',
      ts: '2025-01-01T00:00:00Z',
      payload_hash: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
      role: 'curator',
      sig: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
    },
  ],
});

const playlist = ParseAndValidatePlaylist(rawPlaylist);

console.log(playlist.title);

Parse and validate a channel

import { ParseAndValidateChannel } from 'dp1-js';

const rawChannel = JSON.stringify({
  id: '385f79b6-a45f-4c1c-8080-e93a192adccc',
  slug: 'example-channel',
  title: 'Example Channel',
  version: '1.0.0',
  created: '2025-01-01T00:00:00Z',
  playlists: ['https://example.com/playlist-1.json'],
  signatures: [
    {
      alg: 'ed25519',
      kid: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK',
      ts: '2025-01-01T00:00:00Z',
      payload_hash: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
      role: 'feed',
      sig: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
    },
  ],
});

const channel = ParseAndValidateChannel(rawChannel);

console.log(channel.title);

Sign and verify a playlist (legacy v1.0.x)

ParseAndValidatePlaylist requires either a v1.1.0 signatures array or a legacy v1.0.x signature field. Use ValidatePlaylist(raw, { requireSignatures: false }) to schema-validate an unsigned draft before signing. Signing helpers operate on the unsigned JSON payload (without signature fields):

import { signDP1Playlist, verifyPlaylistSignature } from 'dp1-js';

const rawPlaylist = JSON.stringify({
  dpVersion: '1.0.0',
  title: 'Example Playlist',
  items: [
    {
      source: 'https://example.com/artwork.html',
    },
  ],
});

const privateKey = '0x...';
const publicKey = Buffer.from('...');

const signature = signDP1Playlist(rawPlaylist, privateKey);

verifyPlaylistSignature(rawPlaylist, signature, publicKey);

console.log(signature);
console.log('Signature verified');

For v1.1.0 multi-signature documents, use SignMultiEd25519 / VerifyPlaylistSignatures from the signing API after schema-validating the unsigned payload (ValidatePlaylist(raw, { requireSignatures: false })).

Schedule playback with displayAt

When at least one playlist item includes displayAt, only the current release window should play. Use these helpers to filter items and arm a timer for the next release:

import { computeActiveSet, nextDisplayAt, parseDisplayAt } from 'dp1-js';

const playlist = {
  dpVersion: '1.1.0',
  title: 'Daily',
  items: [
    { source: 'https://example.com/intro.html' },
    { source: 'https://example.com/day1.html', displayAt: '2026-07-21T00:00:00' },
    { source: 'https://example.com/day2.html', displayAt: '2026-07-22T00:00:00' },
    { source: 'https://example.com/day3.html', displayAt: '2026-07-23T00:00:00' },
  ],
};

const now = new Date('2026-07-22T10:00:00Z');
const active = computeActiveSet(playlist, now, 'Asia/Bangkok');
const next = nextDisplayAt(playlist, now, 'Asia/Bangkok');
const release = parseDisplayAt('2026-07-22T00:00:00', 'Asia/Bangkok');

console.log(active.map(item => item.source));
console.log(next?.toISOString());
console.log(release.toISOString());

Timezone rules (Playlist Extension §3.5.2):

  • With Z or a colon offset (+07:00) → absolute instant
  • Without timezone → display-locale wall time (localTimezone, or the device timezone)
  • Date-only (2026-07-21) and compact offsets (+0700) are not accepted
  • DST gap → first valid local instant after the gap; fold → earlier of the two instants

parseDisplayAt throws on malformed input. computeActiveSet / nextDisplayAt skip unresolvable displayAt values (not eligible, not a timer candidate) per §3.5.5.

API Notes

  • parseDP1Playlist(json) returns a { playlist, error } result for already-parsed JSON input (shape-only; not full schema).
  • ValidatePlaylist(data, options?) runs AJV against the core playlist schema. requireSignatures defaults to true; set false for unsigned drafts. Accepts Buffer, JSON string, or a parsed object.
  • ValidateChannel and ValidatePlaylistWithPlaylistsExtension use the same requireSignatures option, as does the deprecated ValidatePlaylistGroup.
  • Leaf helpers such as ValidateNote, ValidateEntity, ValidateDisplayPrefs, ValidateProvenanceBlock, ValidateLocalizedMetadata, and ValidateRefManifest run AJV against the matching schema / $defs (builders use these on build()).
  • PlaylistItemBuilder carries the playlists-extension item fields: .note(), .displayAt(), and .inlineManifest(manifest | RefManifestBuilder). Setting any of them validates the item against the composed core + extension schema instead of core alone, so a malformed inline manifest fails at build().
  • RefManifestBuilder covers the whole manifest: .metadata(MetadataBuilder), .controls(ControlsBuilder), and .i18n({ locale: LocalizedMetadataBuilder }) / .addLocalized(locale, …). The i18n write sites take LocalizedMetadataOverride — LocalizedMetadata with artists / tags / thumbnails closed off — so a full Metadata value cannot stand in for a locale override; reading back gives you a plain LocalizedMetadata. MetadataBuilder has .artists() / .addArtist() and .thumbnails() / .addThumbnail(key, …); ArtistBuilder carries the refVersion 1.1.0 profile — .addresses() / .addAddress(), .avatar(), .biographies() / .addBiography(), .links() / .addLink() — and .url() is deprecated in favour of a links entry of type website; LocalizedMetadataBuilder covers the three localizable fields (title, description, creditLine).
  • Leaf builders (NoteBuilder, DisplayPrefsBuilder, …) and document builders (PlaylistBuilder, ChannelBuilder, RefManifestBuilder, PlaylistItemBuilder, and the deprecated PlaylistGroupBuilder) are exported from the package root. Builder Playlist/PlaylistItem draft shapes stay internal to avoid colliding with the looser parse types exported as Playlist / PlaylistItem.
  • ParseAndValidatePlaylist(data) and ParseAndValidateChannel(data) accept raw JSON as Buffer or string and require signatures (multi-sig or legacy).
  • signDP1Playlist(raw, privateKey) returns a legacy ed25519:<hex> signature string for v1.0.x playlists.
  • verifyPlaylistSignature(raw, signature, publicKey) throws if verification fails.
  • SignMultiEIP191(raw, privateKey, chainID, role, ts) signs with personal_sign semantics and emits the Ethereum-standard 65-byte r || s || v signature (v = 27/28), base64url-encoded, with a did:pkh:eip155:<chainID>:<address> kid. Verification accepts v of either 27/28 (wallets) or 0/1 (dp1-go), so signatures interoperate with wallets and the Go reference in both directions.
  • ParseDPVersion(version) is available for version parsing and major-version checks.
  • DisplayForItem(def, ref, item) merges display preferences using the same field-level overlay order as dp1-go. It takes a single manifest slot, so with both ref and inlineManifest present the caller chooses which to pass; the spec's order is defaults → inlineManifest → ref → item.local, so to honour it fully, call once with the inline manifest and again with the fetched one, feeding the first result forward as def.
  • parseDisplayAt(displayAt, localTimezone?) parses item release times with the timezone rules above; throws on malformed input.
  • parseDisplayAtNanoseconds(displayAt, localTimezone?) returns the exact epoch nanoseconds used by the scheduler; use it when sub-millisecond release times matter.
  • computeActiveSet(playlist, now, localTimezone?) activates displayAt scheduling whenever at least one item has that field; otherwise it returns all items. now accepts a Date (millisecond precision) or epoch-nanoseconds bigint for exact sub-millisecond scheduling. Unresolvable displayAt values are skipped.
  • nextDisplayAt(playlist, now, localTimezone?) returns the soonest future resolvable displayAt. With bigint now, it returns epoch nanoseconds; with Date now, it returns a Date rounded up to avoid early timers.

Edge runtimes (Cloudflare Workers)

Validation never compiles a schema at runtime. AJV normally builds each validator with new Function(...) on first use, which throws Code generation from strings disallowed for this context on workerd and other runtimes that disable dynamic codegen — and only there, so a green Node test run says nothing about it (#24). The schemas are instead compiled to plain JavaScript (AJV standalone) when the package is built, so validation, every builder's build(), and every ParseAndValidate* work unchanged on Workers.

The Worker still needs Node compatibility, as it always has: the package root reaches crypto, net, and dns through the signing and playlist modules, and Buffer is used throughout. Both keys are required in wrangler.toml — the flag alone is not enough:

compatibility_date = "2024-09-23" # or later
compatibility_flags = ["nodejs_compat"]

nodejs_compat only provides the Node built-ins and globals this package needs (including Buffer) from compatibility date 2024-09-23 onward. With an earlier date, the Worker fails to bundle with Could not resolve "crypto" and friends. That configuration — with a current date — is what the smoke test runs and the only one this package is verified on: the library stays Node-targeted, so a plain browser is still out of reach regardless of how validation is compiled.

AJV is a build-time dependency only; installing dp1-js pulls in @noble/curves and @noble/hashes and nothing else. npm run smoke:workerd runs the package inside wrangler dev --local and asserts both an accepted and a rejected document; it also runs in CI.

Schema provenance and parity

Embedded JSON Schema files under src/schema/ track the specification repository, display-protocol/dp1 — core/v1.1.0/schemas/ and extensions/ — and are kept byte-identical to it. Payloads that passed validation under older, looser schemas may fail — for example invalid license values or provenance blocks without type.

Parity with dp1-go is currently partial. The Go SDK's internal/schema/ has not yet picked up two spec changes that this SDK has, so the two implementations disagree on these cases:

| Case | dp1-js | dp1-go | | :-------------------------------------------- | :------- | :------------------------------------- | | Thumbnail with uri only, no w / h | accepted | rejected (required: ["uri","w","h"]) | | Single item with a malformed inlineManifest | rejected | accepted (overlay omits the field) |

Both differences are dp1-js following the current spec, so they should close when dp1-go syncs. Until then, do not assume a document accepted here is accepted by the Go reference. src/schema/core/playlist-group.json is the exception to the provenance rule above: the spec removed the Playlist-Group object (dp1#41), so that file has no upstream counterpart and is retained from dp1-go for backward compatibility.

Playlist-Group is deprecated

The DP-1 spec removed the Playlist-Group (Exhibition) object in dp1#41 — channels superseded it before it saw production use, and per the spec, zero groups were ever published. Every Playlist-Group export here is now marked @deprecated: parsePlaylistGroup, PlaylistGroupDocument, PlaylistGroupBuilder, ValidatePlaylistGroup, ParseAndValidatePlaylistGroup, VerifyPlaylistGroupSignatures, SchemaHooks.PlaylistGroupSchemaValidate, ErrorCode.PlaylistGroupInvalid, and the PlaylistGroup type.

Nothing has changed at runtime — existing documents still parse, validate, and verify exactly as before. Use the channels extension (ChannelBuilder, ValidateChannel, VerifyChannelSignatures) for new work. Removal is deferred to a major release, ideally coordinated with dp1-go, which still ships the object; dropping it here alone would open a fresh parity gap.

Thumbnail dimensions are the one place the schema has since been loosened (display-protocol/dp1#44): w and h are optional on a Thumbnail (only uri is required), so a producer holding a bare thumbnail URL omits them rather than guessing. When present they are still validated as integers ≥ 1. Every document that validated before still validates, but consumers must treat w / h as possibly absent.

The item-level displayAt field follows the Playlist Extension v0.2.0 overlay (display-protocol/dp1 PR #37); it is optional and ignored by older runtimes. playlist.schedule is not part of this extension.

Item-level inlineManifest follows the same overlay (display-protocol/dp1 PR #38): a complete Ref Manifest carried on the item instead of behind a ref URL, for playlists with nowhere to host a manifest document. Same schema and validation as a ref-fetched manifest, and integrity comes from the playlist signature, so no refHash is needed. When an item has both, a consumer resolves ref first; this library stores what you give it and does not drop either.

Both validation paths enforce it: the per-item overlay lives in a single $defs/PlaylistItemExtension shared by playlist_with_extension.json and playlist_item_with_extension.json, so ValidatePlaylistItemWithPlaylistsExtension checks a nested manifest exactly as whole-playlist validation does. (The single-item schema used to omit inlineManifest; fixed upstream in display-protocol/dp1#46, reported from this SDK as dp1#45.)

See CHANGELOG.md for breaking validation changes.

Repo Layout

The repository keeps a familiar module structure in JS:

  • src/playlist
  • src/playlistgroup
  • src/refmanifest
  • src/merge
  • src/sign
  • src/jcs
  • src/extension/*

Development

npm install
npm run lint
npm run type-check
npm test

Validators are generated from src/schema/*.json into src/validate/generated/ by npm run generate:validators, which build, test, and type-check run first (and npm install triggers through prepare). The generated files are build artifacts: they are gitignored, and a schema change is picked up by regenerating, never by editing them. The generator also derives the -unsigned schema variants that { requireSignatures: false } validates against, so those stay in step with the signed ones.

npm run smoke:workerd builds the package, installs it into a throwaway Worker, and exercises it under wrangler dev --local (needs network access for the wrangler install).

npm run check:packaging builds the package and runs publint --strict and attw --pack . over what would be published, so a broken exports map fails here rather than in a consumer's tsc (as #30 did). It runs in CI as the packaging job, and again in publish.yml before the package reaches npm.

The package declares "sideEffects": false, which lets a bundler drop what a consumer does not import — most of all the ~853 KB Ajv validator chunk, which a consumer that only parses documents never touches (bundling parsePlaylist alone goes from ~915 KB to ~1.3 KB). That claim holds only while nothing hand-written in src/ runs at import time, so the built-in ed25519 and eip191 verifiers register on the first read of the registry rather than at module scope; GetVerifier and SupportedAlgorithms populate them before consulting it. A verifier passed to RegisterVerifier always wins, whether it is registered before or after that first read. Note that *Builder.build() schema-validates, so a builder import is not a parse-only import and keeps the chunk. tests/package.test.ts pins the field, the lazy registration, both precedence orderings, and the tree-shake itself — including a bundled, tree-shaken consumer that verifies a real signature.

One import-time side effect remains, in generated code: Ajv writes validateNN.evaluated = {...} metadata at the top level of src/validate/generated/validators.js. Only unevaluatedProperties / unevaluatedItems read it, no schema here uses either, and scripts/generate-validators.mjs fails the build if one starts to — at which point the sideEffects claim has to be narrowed rather than left to mis-validate quietly in a consumer's bundle.

Requirements

  • Node.js 22+
  • npm for dependency installation

Notes

  • This rewrite is intentionally dependency-light.