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

@feralfile/display

v0.6.0

Published

Add a Display on Art Computer button to your website: pair a visitor's browser with their Feral File Art Computer and display a DP-1 playlist on it.

Readme

@feralfile/display

clients/session-recipient/js/ is @feralfile/display, the browser library websites embed to pair a visitor's browser with their Art Computer and display a DP-1 playlist on it. It was published as @feralfile/play through 0.4.0; see Renamed from @feralfile/play. Internally it is the mint-pairing requester: the browser client that asks the Go token minter embedded in FF1 feral-controld for a display grant, a display-only controller record of the FF1 control protocol (ffos-user docs/ff1-control-v1.md §5.7), and then casts to the FF1 over relay v2 with the keys that grant was issued to. An FF1 on FF OS without display grants answers the same request with a relayer session instead, and the library casts with it through the legacy relayer, as 0.4 did; see Both firmware generations.

npm i @feralfile/display

The published package ships compiled ESM + type declarations from dist/ (npm run build). Source stays TypeScript-first in src/.

Browser runtimes check IndexedDB (database ff-play, a name kept so stored grants survive the rename; store displayGrants, keyed by the current website origin) for an existing display grant. If one is missing or expired, requestEphemeralSession generates fresh non-extractable P-256 keys, creates a Mint Pairing Broker channel as the browser (creatorRole: "browser", with the origin from window.location.origin, which the broker checks against the Origin header the browser sets), hands the visitor the app link https://link.feralfile.com/pair?channel=<id>&token=<pairingToken> and a six-digit code through onPairingMaterial, and polls until the visitor's Art Computer joins the channel. It then sends an end-to-end encrypted mint_request carrying the public keys to the joined device's key, polls for the encrypted result, validates the channel binding, stores the grant with its keys and request counter as one IndexedDB record, and returns the pairing metadata. displayDp1Playlist seals a displayPlaylist command under those keys and sends it over relay v2 (wss://<relayV2Endpoint>/v2/channels/<deviceId>/connections), opening a socket per cast and closing it right after, without exposing the envelope to website code. See Sequential Flow for the end-to-end model.

import {
  displayDp1Playlist,
  requestEphemeralSession
} from "@feralfile/display";

const session = await requestEphemeralSession({
  brokerBaseUrl: "https://handoff.feralfile.com",
  browserInfo: { name: "Chrome", label: "Gallery wall browser" },
  // Show the link (button or QR) and the code; do not log them.
  onPairingMaterial: ({ appLink, shortCode, expiresAt }) => showPairing(appLink, shortCode, expiresAt)
});

await displayDp1Playlist({ session, playlist: dp1Playlist });

Wrapped Pairing UI

For a standard integration, mount the provided Display on Art Computer button. It checks the origin's grant store first, opens the pairing dialog only when there is no valid grant, waits for the Art Computer to join and the owner to approve in the app, and then sends the DP1 playlist to the FF1 over relay v2.

import { mountDisplayOnArtComputerButton } from "@feralfile/display";

mountDisplayOnArtComputerButton({
  container: "#display-on-art-computer",
  playlist: dp1Playlist,
  brokerBaseUrl: "https://handoff.feralfile.com",
  // Where a relayer session casts when the FF1 names no relayer of its own;
  // only FF1s on FF OS without display grants use it.
  relayerBaseUrl: "https://tv-cast-coordination.autonomy-system.workers.dev",
  // Optional; this is the default.
  appLinkBaseUrl: "https://link.feralfile.com/pair"
});

The relay v2 endpoint arrives inside the grant (relayV2Endpoint) and is the only one a grant uses, so never hard-code a relay v2 host in display code. A Content-Security-Policy still has to be written before any pairing exists, so connect-src needs the hosts up front: the Integration Guide lists them.

On a touch device (phone or tablet) the dialog shows an Open the Feral File app link and, below it, the six-digit code, which copies on tap. On a pointer device it shows a QR code of the app link, drawn in the page with the bundled qrcode-generator (no image request), captioned "Scan with your phone camera or the Feral File app", with the code below. A status line reads "Waiting for your Art Computer…" and then "Approve in the Feral File app…". If the channel expires before an Art Computer joins, the dialog shows fresh material, up to twice, and then the button fails with approval_timeout.

When the FF1 answers a cast with an unverified unknown_controller or bad_envelope (a factory reset, or the site was removed on the device), the button fails with device_unrecognized, keeps the stored grant, and reads "Pair again?"; only the visitor's next click clears it and pairs again. When the legacy relayer answers a relayer session's cast with 400 {"error":{"id":1003}} (no FF1 on the session's channel: updated, or off), the button also reads "Pair again?", but that click pairs without deleting the session; only a successful pairing replaces it.

For custom UI, use createPairingDialog, requestEphemeralSessionWithPairingUi, hasStoredEphemeralBrowserSession, clearStoredEphemeralBrowserSession and unpairArtComputer. The dialog accepts copy, class-name, and layout overrides so a website can keep its own styling while preserving the pairing sequence and approval handoff; createDialog replaces it with any implementation of PairingDialog ({ show, setStatus, close }).

Both firmware generations

One pairing request serves every FF1. It carries the display-grant keys, and the FF1 answers with what its firmware can issue:

  • FF OS with display grants: a display grant. EphemeralBrowserSession is { kind: "displayGrant", deviceId, ctl, expiresAt?, persistent }, the grant and its keys live in IndexedDB, and casts go over relay v2.
  • FF OS without display grants (2.0.x): its minter ignores the keys and mints a relayer session. EphemeralBrowserSession is { kind: "relayerSession", sessionId, expiresAt?, persistent, relayerBaseUrl? }; the token is stored in localStorage under ephemeralBrowserSessionStorageKey(origin) (the 0.4 key and record, so a session 0.4 stored keeps working) and never appears in the returned value. A cast is one HTTPS POST /api/cast on the session's relayerBaseUrl, or the relayerBaseUrl option when the session names none; with neither, the cast throws.

On each display request the library takes a stored grant first, then a stored session, and pairs only when neither is valid. A grant replaces any stored session. When an FF1 that paired on 2.0.x moves to FF OS with display grants, the legacy relayer answers the old session's cast with 400 {"error":{"id":1003}}; the library throws device_unrecognized and keeps the session, because an old FF1 that is only switched off answers the same. The button then reads "Pair again?", and that pairing yields a grant.

Renamed from @feralfile/play

The verb for putting art on a display is display, not play, so the package is @feralfile/display from 0.5.0 and the button reads Display on Art Computer. @feralfile/play stops at 0.4.0. To move, change the dependency and the import specifier, then follow Upgrading from 0.4.x. The old names were deprecated aliases in 0.5.x and are gone in 0.6.0:

| Removed in 0.6.0 | Use | | :-- | :-- | | mountPlayOnArtComputerButton | mountDisplayOnArtComputerButton | | PlayOnArtComputerButtonOptions | DisplayOnArtComputerButtonOptions | | PlayOnArtComputerButtonHandle | DisplayOnArtComputerButtonHandle | | PlayError | DisplayError | | PlayErrorCode | DisplayErrorCode | | CSS classes ff-ac-play, ff-ac-play-button, ff-ac-play-status | ff-ac-display, ff-ac-display-button, ff-ac-display-status |

What reads differently: the default button label is "Display on Art Computer", the dialog title "Display on your Art Computer", the messages that told the visitor to press play now say try again, and an error's name is "DisplayError". buttonLabel and dialog.copy overrides work as before, and error codes are unchanged. The IndexedDB database keeps the name ff-play. The self-hosted bundle is display.js; only 0.5.x releases also attached it as play.js.

Upgrading from 0.5.x

0.6.0 pairs with every FF1 instead of failing older firmware with device_unsupported. EphemeralBrowserSession is a union discriminated by kind ("displayGrant" or "relayerSession"); code that reads deviceId or ctl checks kind === "displayGrant" first. relayerBaseUrl is an option again on DisplayDp1PlaylistOptions and the button (no default), and DisplayDp1PlaylistOptions takes fetchImpl again for the relayer cast. A stored 0.4 session is kept and used, no longer removed. The deprecated Play aliases, the ff-ac-play* class names and the play.js release asset are gone (table above). See CHANGELOG.md.

Upgrading from 0.4.x

Move straight to 0.6: it keeps the visitor's stored 0.4 session, and an FF1 on FF OS with display grants is paired again with a grant the first time the old session stops reaching it. What changed against 0.4 is below (written for 0.5.0; 0.6.0 brings back the relayer session, relayerBaseUrl, fetchImpl and ephemeralBrowserSessionStorageKey described above).

0.5.0 is a breaking release. The library casts only with a device-issued display grant over relay v2; there is no relayer token, no /api/cast and no relayerBaseUrl. EphemeralBrowserSession is now { deviceId, ctl, expiresAt?, persistent }, the grant lives in IndexedDB (the storage option is a boolean; false makes pairing fail with storage_unavailable), hasStoredEphemeralBrowserSession(origin) and readStoredEphemeralBrowserSession(origin) are async and take only the origin, as does clearStoredEphemeralBrowserSession(origin); storeEphemeralBrowserSession, ephemeralBrowserSessionStorageKey, TokenStorage and TokenStorageOptions are gone (storage is a GrantStorageOptions boolean). DisplayDp1PlaylistOptions loses fetchImpl and relayerBaseUrl and gains socketFactory and timeoutMs. Stored 0.4 session records are removed on first use, so each visitor pairs once more. 0.5 needs an FF1 on FF OS with display grants; older firmware fails with device_unsupported.

Commands

npm ci
npm run lint
npm run typecheck
npm test
npm run build
npm run bundle

npm test runs the FF1 control protocol v1 fixtures vendored under test/fixtures/ff1-control-v1 (see SOURCE.md there) against the codec, and casts against a stub relay v2 channel with a stub FF1 that opens every envelope with the fixture device keys.

Boundaries

  • Keep a grant's private keys non-extractable and only in origin-scoped IndexedDB, together with the grant and its request counter.
  • Keep a relayer session's token only in origin-scoped localStorage; never return it from the public API or put it in a thrown error.
  • Do not expose key material or the pairing token through logs, thrown errors, analytics, or public callbacks. The one exception is the channel's pairing token inside the app link, which onPairingMaterial hands over because the visitor has to open or scan it; it is single-use and dies with the channel.
  • A grant seals only displayPlaylist in the Play shape (the FF1 control protocol's name for that command form) and its own revokeController; the FF1 refuses everything else with permission_denied.
  • Connect to relay v2 per cast and close at once; never hold an idle socket.
  • Keep API names requester-oriented rather than tied to a specific website.