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

@layr8/broker-client

v0.2.0

Published

A Layr8Client with @layr8/sdk's shape that speaks through the local layr8 broker daemon, on a temporary child DID or on this agent's persistent identity

Readme

Broker local client

A Layr8Client with the shape of @layr8/sdk's, for a program that wants to use the local layr8 broker daemon's node connection instead of opening its own. The design is ADR 0017, section 2. The wire is docs/specs/local-client-protocol.md.

Published as @layr8/broker-client. This version, 0.2.0, is a stable release on the npm latest dist-tag: npm i @layr8/broker-client resolves it. Prereleases go to next and have to be pinned; this one does not.

The daemon line does not follow this number. @layr8/mcp is 0.5.0. The two lines are versioned separately and always have been — this one reached 0.2.0 through two prereleases and the daemon line reached 0.5.0 through one, so the suffixes they passed through never matched.

Install the layr8-broker 0.5.0 line, the daemon this release is published beside. It is the first that offers this agent's persistent identity (identity.fixed) and the read of that identity's credential store (credentials.fixed). An older daemon refuses both at hello, by capability name, before anything is joined.

A default install resolves it:

npm i -g @layr8/mcp
# or a binary:
curl -fsSL https://raw.githubusercontent.com/layr8/broker/main/install.sh | sh

A binary already installed on the default latest channel updates itself to 0.5.0 at its next check; nothing has to be asked for by name.

The oldest daemon that serves this client's protocol at all is 0.4.0-rc.1 (MIN_BROKER_VERSION), and that has not changed. One older than that fails connect with BrokerProtocolError, naming the version. A layr8-broker 0.4.0 daemon does serve a BORROWED session of this release — it answers the join without saying which identity it served, and client.identity reads unstated rather than guessing.

MIN_BROKER_VERSION is a word in those two messages and is never compared to anything: what gates is hello, the protocol version and the capability list, which are what a daemon answers about itself. A version number would have discriminated nothing for the defect that made this rule — both daemons reported 0.4.0 — and would be wrong here, because 0.4.0 works for a borrowed session.

A daemon that is already running is used as it is, so stop an older one first. Avoid 0.4.0-rc.1: that binary replaces itself with 0.3.0, which cannot serve this client.

Use

// the only line that changes
import { Layr8Client, logErrors, randomChildSegment } from '@layr8/broker-client'

Every name @layr8/sdk exports is exported here unchanged, except Layr8Client and mediation.

There is one path and no switch. Layr8Client is always backed by the broker daemon for LAYR8_MCP_ENV (default dev), and there is no fallback: a program that wants its own node connection uses @layr8/sdk directly.

| LAYR8_VIA_BROKER | What happens | | --- | --- | | unset or "" | nothing; it is not an ask | | 1 | nothing; it asks for what happens anyway | | 0 | constructing a client throws, naming the variable. It used to select the direct path, which is gone, and silently giving it the broker would put the session somewhere nobody chose | | anything else | constructing a client throws |

A configuration written for the old mode

LAYR8_NODE_URL and LAYR8_API_KEY configured a launcher's own sign-in to a node. Nothing signs in with them here — the broker signs in with its own enrolment — so constructing a client fails, naming whichever of the two is set and giving the two commands that replace them:

LAYR8_NODE_URL and LAYR8_API_KEY are set, and nothing signs in to a node with them any more.
A session now goes through the layr8 broker on this machine, and the broker signs in with its
own enrolment. Two steps replace them:
  1. layr8-broker enrol --name <label> --node <wss-url> --api-key <key>
     The node and the key are the two values above, so they move across as they are.
  2. Point your launcher at the profile <label>. Each launcher asks for it its own way, so
     the command is in that launcher's own README.
Then remove LAYR8_NODE_URL and LAYR8_API_KEY from the launcher's environment.

Step two names no command on purpose. The launchers do not share an installer — some register a profile at install time, some take one per session — and this package cannot run or check any of those spellings, so a command printed here would read as something somebody verified. If you maintain a launcher and want the refusal to name yours, that spelling has to come from your repository.

Either of them set to nothing is not an ask and passes: a blank line in a .env configures no sign-in. LAYR8_AGENT_DID is not refused — it still names the DID a session asks for. It is the environment that is refused and not nodeUrl / apiKey in the config object, which is a launcher's own source and not what those two commands replace.

If no daemon listens on ~/.layr8/broker-<env>.sock, the client starts one: $LAYR8_BROKER_BIN daemon, or layr8-broker daemon from PATH when LAYR8_BROKER_BIN is unset. If that program cannot be run, or starts and does not listen within 10 seconds, connect rejects with BrokerUnavailableError (a ConnectionError) and the daemon's log.

Which identity a session gets

It is a field, never the shape of the config.

// a temporary child of the enrolled agent DID — the default
new Layr8Client(onError, { didSpec: { parentDid: agentDid, mode: 'Create' } })

// this agent's persistent identity: the enrolled agent DID itself
new Layr8Client(onError, { identity: 'fixed', agentDid })

| identity | What is joined | | --- | --- | | absent or 'borrowed' | a child DID named beneath didSpec.parentDid. Name the segment with agentDid, or leave it empty and the SDK's rule generates one here | | 'fixed' | this agent's persistent identity: the enrolled agent DID, bound by the daemon and lent to one session at a time. did after connect is that DID |

client.identity is what the daemon SAID it served, read from the join result. It has three values and an absence, and no two of them mean the same thing:

| client.identity | What it means | What to do about it | | --- | --- | --- | | undefined | no join has answered | connect | | 'unstated' | a join answered and the daemon did not say — a layr8-broker older than the field serves the same protocol version and omits it. layr8-broker 0.4.0 is exactly that daemon, it is what is installed wherever nothing has been upgraded, and this client starts one from PATH when none is listening | upgrade the daemon to the 0.5.0 line; connecting again changes nothing | | 'borrowed' / 'fixed' | the daemon said so | nothing |

This client never fills the value in from what it asked for, and never reads 'unstated' as undefined: telling a connected session to connect is an instruction that cannot help.

mediation.* refuses with a different sentence for each of the first three.

An agentDid with no didSpec.parentDid is not an ask for either. Earlier versions of this package turned that shape into a borrowed child of that DID — a temporary identity, under a configuration that looks like it names a stable one. It does not any more: the join goes out as configured, the daemon refuses it no-parent, and the refusal names the two asks that exist.

A fixed join is refused, before anything is sent, when it asks for something the daemon's own binding cannot do. Each refusal arrives as a BrokerJoinRefusedError — a ConnectionError, so an existing catch is unchanged — whose refusal is the reason:

| refusal | What it means | | --- | --- | | fixed-identity-not-configured | this profile offers no persistent identity. layr8-broker enrol --persistent-identity | | fixed-identity-not-bound | it is configured and the binding did not succeed. bindingState says which of the causes it was | | fixed-identity-held | another session has it; holder and since name who and when | | fixed-identity-mismatch | the join also named a didSpec.parentDid, or an agentDid that is not the identity | | fixed-identity-type-undeliverable | a handle(type, …) for a type the daemon's binding cannot deliver | | fixed-identity-protocol-unbound | a handler type whose protocol that binding did not bind |

The last two exist because the identity's join is the daemon's, made once before any session, and never remade: a handler or a protocol added from a session would never fire while every step reported success.

Read the refusal's fields, not its sentence. Beside refusal, a BrokerJoinRefusedError carries fixedDid, holder, since and bindingState — everything the daemon put on the refusal, each as a field, so a launcher that draws held by X since Y never matches a regular expression against message. The sentence is free to be reworded; a field is not.

Each of the four has three readings, never two:

| Reading | The field | stated | | --- | --- | --- | | absent — the daemon put nothing here | undefined | does not contain the name | | said — the daemon's own string, verbatim, "" included | that string | contains the name | | unreadable — the daemon sent something that is not a string | undefined | contains the name |

An absent field is never the calm answer. An absent fixedDid means this daemon cannot name the identity's DID, not that the DID does not matter; an empty holder is an anomaly — the field is documented never empty — and not "nobody holds it". refusalData is the payload as it crossed the socket.

What works through the daemon

The members a launcher uses, with the SDK's arguments and return shapes: new Layr8Client(onError, cfg), handle before connect, connect, close, did, send, request (thread correlation, parentThread, signal), on('disconnect' | 'reconnect'), delegatedCredentials and supportsEphemeralDelegation.

  • The borrowed DID must be named beneath the daemon's enrolled agent DID: didSpec.parentDid is that DID, and agentDid is it plus one segment you choose (for example randomChildSegment()). Left empty, the SDK's rule generates the segment in this process. When parentDid is not the daemon's enrolled agent DID, connect rejects with a ConnectionError whose refusal is parent-not-enrolled.

  • mediation.* runs only for a session holding the persistent identity. That DID's join binds both mediation protocols, so the steps that go over the wire work. The steps that read the node's REST store or post to /didcomm (declare, undeclare, reinject, and collect / pickup through them) do not: the daemon holds this agent's enrolment. They fail with a sentence saying so, in the shape each step reports anything else. The daemon arms the Space's mediator for this identity on every claim by itself — layr8-broker doctor reports where that stands — so a launcher usually has nothing to call here. A client on a borrowed child is refused at the call, and one that cannot yet tell which identity it is on is refused with a different sentence again: "which identity" is not a question this client answers before the daemon has.

    These two refusal shapes are not the same, and a caller has to handle both. The steps' own failures are values — { ok: false, error }. This package's identity check is a synchronous throw of BrokerCapabilityError, because it sits in the wrapper in front of the step rather than inside it. So const r = await mediation.status(c, m); if (!r.ok) does NOT catch a borrowed session: it throws before it returns. Wrap the call, or check client.identity === 'fixed' first.

  • nodeUrl and apiKey are not used. The daemon connects with its enrolment.

  • Grants are attached by the SDK in the daemon, from the credentials the node delegated to your DID at the join.

  • handle(type, fn) binds type's protocol at the join, so replies of that protocol reach request, and runs fn for messages nobody is waiting for. A reply reaches your DID only on a protocol you bound, exactly as with the SDK directly.

  • disconnect and reconnect are raised when the daemon's node connection drops and comes back, and when the socket to the daemon does. After a socket loss the client dials again with the SDK's backoff, starting the daemon if needed, and joins the same DID before it raises reconnect.

What does not, and fails with the capability's name

| You call | Fails | Capability | | --- | --- | --- | | handleAll | connect | handlers.catch-all | | handle(..., {manualAck: true}) | connect | handlers.manual-ack | | a mediator (or LAYR8_MEDIATOR_DID) | connect | mediation | | mediation.* on a client that is not holding the persistent identity, or that cannot tell whether it is | the call, in that step's own shape — see below | mediation | | the credential calls other than listCredentials (getCredential, storeCredential, signing, verifying, presentations) | the call | credentials | | listCredentials on a borrowed child, or against a daemon that does not serve the read | the call | credentials, credentials.fixed | | joinDid, leaveDid, refreshGrants, mcp | the call | the member's name |

A mediation step refused because of which identity the client holds is reported the way that step reports anything else: enroll, declare, undeclare, pickup, live, status and bootstrap answer { ok: false, error }, so the documented const r = await mediation.status(c, m); if (!r.ok) sees it. reinject and collect throw, because their results carry no failure of their own — reinject's ok is a list of ids, so { ok: false } there would read as a failure to if (!r.ok) and as a crash to r.ok.length. @layr8/sdk draws the same line for the same reason.

listCredentials is answered for this agent's persistent identity: that identity is the enrolled agent DID, it holds its own grants in the node's credential store, and the daemon — which holds the enrolment — reads them for the session holding it. You get the node's records, in the SDK's StoredCredential shape.

It refuses for three different reasons, because the reader's next step differs every time. A borrowed child has no credential store of its own: what it can spend is delegatedCredentials(). A daemon older than this read does not offer credentials.fixed, serves the same protocol version and says nothing else about it, so the refusal names the upgrade. And a session whose identity the daemon did not state is not guessed at.

An empty array means the node answered and this identity has nothing it can present. A read that did not happen throws BrokerCredentialReadError, which is a class and not a sentence, so the two cannot be collapsed by accident: an identity reported as having nothing because a request failed is an agent invented out of a failed read.

An empty array is not "this identity was granted nothing", and you must not print that for it. The node's read leaves out a credential it knows to be revoked, so an identity that was never granted anything and an identity whose every grant has been revoked both come back as []. Nothing on this path can separate them. For the same reason, a record you do get is "not known to be revoked" rather than "proved live": that filter fails open, so a credential the node could not judge is still returned.

timeoutMs is passed to the daemon, which makes the read with fetch, so it is a total deadline. The SDK's ListCredentialsOptions.timeoutMs, whose type you are reading, is a socket-INACTIVITY deadline. Same name, same default (30s), different measurement — raise it here for a slow read, not for a slow first byte.

Differences you can observe:

  • A request still waiting when the socket to the daemon drops rejects with NotConnectedError. The SDK keeps such a request waiting across a node reconnect.
  • The daemon does not replace a daemon of an older version, as the shim does. A daemon too old for the protocol fails connect with BrokerProtocolError.

Dependencies

@layr8/sdk is a peer dependency, >=0.4.1 <0.5.0. Install it yourself; this package uses your copy and never brings its own.

There must be exactly one copy of the SDK in your program. Its error classes, such as NotConnectedError, are checked with instanceof, and a class from a second copy fails that check against the first: a disconnect reads as some other failure. This package re-exports the SDK's own classes, so an error it raises is an instance of the class you import from either package.

npm refuses to install this package beside an SDK outside the range (for example ^0.3.0) with ERESOLVE, instead of nesting a second copy.

The tests run against 0.4.1, the lower bound. test/single-sdk-copy.test.ts installs the packed package into a program that uses another SDK version in the range, and fails if a nested copy appears.