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

@metalabel/dfos-web-relay

v0.54.0

Published

DFOS Web Relay — verifying HTTP relay for identity chains, content chains, and content blobs

Readme

@metalabel/dfos-web-relay

Relays verify everything they receive and serve everything they've verified. Authorship is verifiable without trusting any server, and which view of an identity you follow is a choice of relay. No hierarchy, no central authority, topology is emergent. Portable HTTP relay for the DFOS protocol.

See RELAY.md for the full relay specification.

Install

npm install @metalabel/dfos-web-relay @metalabel/dfos-protocol hono

@metalabel/dfos-protocol and hono are peer dependencies. Hono is peered rather than bundled because the relay's public API is Hono-typed — createRelay returns { app: Hono } and serve(app) takes one — so the relay and your app must resolve to the same Hono install. A private copy yields two structurally incompatible Hono types and the handoff fails to compile.

Usage

Embedded (Hono app)

import { createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';

const relay = await createRelay({
  store: new MemoryRelayStore(),
});

// relay.app  — Hono application
// relay.did  — the relay's auto-generated DID
// relay.syncFromPeers() — pull operations from configured peers
// relay.projectIndex()  — advance the /index/v0 projection by one budget

export default relay.app;

Set signing: true to enable the optional signing mailbox; it is disabled by default. The store must implement SigningStore or createRelay throws signing capability requires a store implementing SigningStore.

Set authority to the host[:port] callers reach this relay at. It is what every API-AUTH identity proof is checked against, and it is configuration, never read from a request header — without it the authenticated routes answer 503. ingestion (open | proof-required | closed) and an injectable admissionPolicy set who may submit operations (RELAY § Admission).

Advertising an OpenAPI document

Serving an OpenAPI document is a SHOULD, so it is opt-in. openapi: { document } serves the document at /openapi.json (override with route) and advertises that path in the well-known's openapi field; openapi: { url } advertises a document hosted elsewhere without registering a route. Absent the option the relay serves none and omits the field. This package's own document — the one describing the route table below — ships as an importable JSON artifact generated from openapi.yaml:

import document from '@metalabel/dfos-web-relay/openapi.json';

const relay = await createRelay({ store, openapi: { document } });

The document is discovery, never authority: the routes, capability gates, and auth rules the spec fixes govern regardless of what an advertised document says.

Standalone (Node.js)

import { serve } from '@metalabel/dfos-web-relay/node';

serve({ port: 4444 });

Routes

| Method | Path | Description | | ------ | ------------------------------------------- | -------------------------------------------------------------------- | | GET | /.well-known/dfos-relay | Relay metadata (DID, capabilities, profile, peers, stats) | | GET | /openapi.json | The configured OpenAPI document (registered only when configured) | | POST | /proof/v1/operations | Submit signed operations (identity, content, countersig) | | GET | /proof/v1/identities/:did | Get identity chain terminal state | | GET | /proof/v1/identities/:did/log | Paginated identity chain operation log | | GET | /proof/v1/content/:contentId | Get content chain terminal state | | GET | /proof/v1/content/:contentId/log | Paginated content chain operation log | | GET | /proof/v1/log | Paginated global operation log (log capability) | | GET | /proof/v1/operations/:cid | Get a single operation by CID | | GET | /proof/v1/countersignatures/:cid | Paginated countersignatures for any CID (ops, artifacts) | | GET | /1.0/identifiers/:did | Resolve a did:dfos to a W3C DID Document (DIF-compat) | | GET | /revocations/v1/credential/:credentialCID | Revocation status for a credential (self-proving JWS) | | GET | /revocations/v1/issuer/:did | Paginated feed of all revocations ingested for an issuer | | POST | /signing/v0/requests | Deposit a sign request (signing capability; 501 when disabled) | | GET | /signing/v0/requests | Poll pending sign requests (signing capability; 501 when disabled) | | POST | /signing/v0/requests/:cid/response | Submit a sign response (signing capability; 501 when disabled) | | GET | /signing/v0/requests/:cid/response | Poll sign response status (signing capability; 501 when disabled) | | POST | /signing/v0/requests/:cid/decline | Decline a sign request (signing capability; 501 when disabled) | | GET | /index/v0/operations | Browse operation metadata rows by recency (index capability) | | GET | /index/v0/identities | Query materialized identity projections (index capability) | | GET | /index/v0/content | Query materialized content projections (index capability) | | GET | /index/v0/artifacts | Query standalone signed artifacts (index capability) | | GET | /index/v0/countersignatures | Query countersignatures by witness (index capability) | | GET | /index/v0/credentials | Query credential projections (index capability) | | GET | /index/v0/credits | Query credits on public head documents (index capability) | | PUT | /content/:contentId/blob/:operationCID | Upload blob (identity proof required) | | GET | /content/:contentId/blob | Download blob at head (public grant, or identity proof + credential) | | GET | /content/:contentId/blob/:ref | Download blob at specific operation ref |

Route Semantics

The route table above is the full surface this package serves; the semantics behind it are the spec's to define, not this README's. DID resolution (/1.0/identifiers/:did) follows the normative mapping in DID-METHOD.md §4; revocation status (/revocations/v1/*) is specified in RELAY § Revocation status; blob upload/download authorization is RELAY § Access.

Peering

Relays replicate operations via three composable per-peer behaviors — gossip-out, read-through, sync-in — specified in RELAY § Peering; operator guidance for running a peered relay is at protocol.dfos.com/deploy.

import { createHttpPeerClient, createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';

const relay = await createRelay({
  store: new MemoryRelayStore(),
  peerClient: createHttpPeerClient(),
  peers: [{ url: 'https://other-relay.example.com' }],
});

The PeerClient is injected like the store — semantic per-resource methods, not raw HTTP. The default (createHttpPeerClient) uses HTTP; tests inject mocks that route directly to another relay's API in-process:

interface PeerClient {
  getIdentityLog(
    peerUrl: string,
    did: string,
    params?: { after?: string; limit?: number },
  ): Promise<{ entries: PeerLogEntry[]; next: string | null } | 'invalid-cursor' | null>;

  getContentLog(
    peerUrl: string,
    contentId: string,
    params?: { after?: string; limit?: number },
  ): Promise<{ entries: PeerLogEntry[]; next: string | null } | 'invalid-cursor' | null>;

  // `null` = transport/peer failure; `'invalid-cursor'` = the peer explicitly
  // rejected `after` (400) — the distinguished value the sync loop's self-heal
  // requires. A client that collapses the 400 into `null` leaves the puller
  // retrying a dead cursor forever after a peer wipes or rebuilds its log.
  getOperationLog(
    peerUrl: string,
    params?: { after?: string; limit?: number },
  ): Promise<{ entries: PeerLogEntry[]; next: string | null } | 'invalid-cursor' | null>;

  submitOperations(peerUrl: string, operations: string[]): Promise<void>;
}

A PeerLogEntry is { cid: string; jwsToken: string }. The 'invalid-cursor' outcome (a peer's 400 cursor rejection, distinct from transport failure) is load-bearing for the sync loop's self-heal — see RELAY § Peering.

Implementing a store

A store implements one required contract and, optionally, up to three more. Which ones it implements is what the relay reads its capabilities from — there is no capability flag that can promise something the store cannot do, and no member that exists only to throw.

import { createRelay } from '@metalabel/dfos-web-relay';

// a read-only relay: serves the proof plane, the content plane, the log and the
// revocation routes. `write` is false, `index` is false, and it says so.
const relay = await createRelay({ store: myReadStore, identity: myIdentity });

RelayReadStore — required

Every read a route performs: operations, identity and content chains, chain state at an arbitrary CID, blobs, countersignatures, the paginated global log, stats, revocations, and held public credentials. A relay over nothing but this serves every GET the spec defines.

Reads FAIL CLOSED. A read that cannot be answered throws; it never returns undefined to mean "the store is unwell". Absence and failure are different answers, and ingestion classifies them differently — absence is a verdict, a throw is retryable.

RelayWriteStore — one method

interface RelayWriteStore extends RelayReadStore {
  commit(batch: CommitBatch): Promise<CommitResult>;
}

CommitBatch is a typed, exhaustive description of everything ONE accepted operation implies — the operation row, the identity or content chain's new head and log, the global-log append, a countersignature, a revocation, a standing credential added or (issuer-scoped) dropped — or one document blob. The store persists all of it or none of it, and answers new or duplicate.

Atomicity is the contract. A partial commit is a corrupt relay: an operation in the operation table but not in the log is invisible to every puller forever, and a chain head advanced without its operation row breaks fork verification. If the commit throws, the store MUST have persisted nothing; the relay treats a throw as retryable and keeps the raw operation for a later pass.

A writing relay also holds writer-internal bookkeeping — the raw-operation buffer the sequencer drains, and per-peer sync cursors (RelayWriterState). That is this package's own state, not part of the store contract a read-only integration has to care about.

IndexReadStore / IndexWriteStore — the index profile

IndexReadStore is the nine queries behind /index/v0, pushed down so a page costs O(page). IndexWriteStore is the projection side: applyIndexRows plus a persisted cursor.

They are separate because a store can implement one without the other. A store whose index rows are maintained by an external worker implements the queries, advertises index: true, and the relay does no projection work for it. A store that implements both lets this package run the projection:

import { projectIndex } from '@metalabel/dfos-web-relay';

// inline (default): the relay drains the projection after each accepted batch
await createRelay({ store });

// external: nothing runs it but you
const relay = await createRelay({ store, indexProjection: 'external' });
setInterval(() => void relay.projectIndex(), 5_000);

The projection walks the operation log from its cursor, maps each entry to the rows it dirties, and applies them. It is never inside a commit, every pass is bounded by a budget, and a full-corpus fan-out (a chain:* grant, an identity delete or restore) is carried on the cursor as a resumable sweep rather than drained in one pass.

external moves the LOG drain to your worker; it is not "the relay runs no projection". A document blob arrives on its own route and nothing on the operation log marks that moment, so the relay recomputes the rows that project that document itself, in both modes, right after the blob commits. That pass is bounded by a reverse lookup on the documentCID — no fan-out, no log read.

The index is a projection OF THE LOG, so log: false turns it off for a store this relay maintains: createRelay throws on an explicit index: true and advertises index: false otherwise, rather than serving empty pages forever. A store that implements only IndexReadStore is maintained elsewhere and keeps its index either way.

SigningStore — the optional mailbox

The ephemeral courier state behind /signing/v0. signing: true over a store that does not implement it is a configuration that lies, so createRelay throws.

MemoryRelayStore implements all of them, and is the reference implementation.

Migrating a store written against the old interface

This is a breaking change to the package's store API (0.x, so a minor bump). A store written against the single RelayStore interface needs three edits:

  1. Replace the ~14 write members with commit. putOperation, putIdentityChain, putContentChain, putBlob, addCountersignature, appendToLog, addRevocation, addPublicCredential and removePublicCredential are gone. One commit carries what all of them carried, and the store decides how to make it atomic. removePublicCredential is now issuer-scoped: drop the held credential only when its issuerDID matches the one on the batch.
  2. Replace the putIndex* members with applyIndexRows, and add the cursor. getIndexCursor / setIndexCursor persist where the projection got to. If your index is maintained elsewhere, implement neither and keep the queries.
  3. Return ingestedAt on log entries, and delete getIndexOperationRow. A LogEntry now carries the relay's receipt stamp for that operation. It is the single clock read per operation and the projection's only source for it, which is what the optional getIndexOperationRow used to be for. It is store state, not wire state: GET /proof/v1/log still serves {cid, jwsToken, kind, chainId}.

Members that were optional and are now simply absent (getStats is required; getRevocations was unused and is deleted) and the members a read-only store used to answer by throwing can all be removed.

Two more renames a caller sees: the exported isDependencyFailure is now isRetryableRejection (it covers a momentary store fault as well as a missing dependency, and both mean "keep the raw operation and try again"), and IngestionResult carries storeFault alongside dependencyMissing.

A read-only store (RelayReadStore with no commit) also needs three things from its deployment: getStats is required rather than optional, identity must be passed to createRelay (there is nowhere to write a JIT genesis), and no peers may be configured (peer sync writes).

License

MIT