@fleetmesh/leaf
v0.2.2
Published
Fleetmesh leaf profile in TypeScript: browsers and PWAs as mediated mesh leaves with NIP-59 gift wraps, DIDComm v2, and NIP-77 Negentropy reconciliation
Downloads
1,024
Maintainers
Readme
@fleetmesh/leaf
The Fleetmesh leaf profile in TypeScript, so browsers and PWAs can join the mesh as mediated leaves.
A leaf holds one wss:// connection to its mediator (an anchor), speaks
gift-wrapped DIDComm v2 over standard Nostr frames, reconciles with NIP-77, and
keeps a bounded local store. It does not forward, mediate, issue grants or file
reports. See fleetmesh.org/spec.html, "The leaf profile and mobile
operational limits" and "Mediation is how a phone gets an address".
Conformance
The Python reference in core/ is the source of truth. Every primitive here is
tested against core/vectors/*.json, read in place through the umbrella
layout, so this package must be checked out next to core/.
npm install
npm test # Node
npm run typecheckThe same suite runs in the three engines a PWA meets, through vitest browser mode on Playwright:
npm run browsers # once: Chromium, Firefox and WebKit
npm run test:browserPlaywright keeps its engines in a shared cache guarded by a lock. If another
tool's install is holding that lock, give this package a cache of its own by
setting PLAYWRIGHT_BROWSERS_PATH for both commands, e.g.
~/Library/Caches/ms-playwright-fleetmesh-leaf.
Dependencies are limited to the audited @noble/* and @scure/base
libraries, pinned exactly.
Negentropy interop
scripts/negentropy-harness.ts speaks hoytech's negentropy conformance
protocol (the same one core's fleetmesh_core.conformance.harness speaks), so
fleetmesh.org/scripts/interop-negentropy.py can put this port on one end of a
reconciliation and the Python reference on the other. src/ uses erasable
TypeScript only, so Node runs it without a build step:
node scripts/negentropy-harness.ts < commandsEnd to end against a real anchor
npm run e2e starts meshnode's own relay server and a NodeApp of class
anchor on localhost (scripts/e2e-anchor.py), then drives a LeafSession
against it over a real WebSocket: descriptor exchange, mediate-request and
grant, keylist update, forwards buffered by the anchor and drained in
batches across every registered key, acks purging the inbox, and a
dropped socket resumed with the mail that arrived meanwhile picked up.
Needs Python 3 and the umbrella's meshnode/, core/ and schema/.
The same lifecycle runs inside Chromium, Firefox and WebKit, each leaf on the
page's own WebSocket and crypto, against one shared anchor that a vitest
global setup starts (test-e2e/):
npm run test:browser-e2eBuffering a forward and reading the anchor's queue are the anchor's side, so the page asks for them through vitest browser commands; everything the leaf does happens in the browser.
In a browser: the PWA pieces
import { startBrowserLeaf } from '@fleetmesh/leaf';
const hub = startBrowserLeaf({ mediator: { pubkey: '<anchor node key>', endpoint: 'wss://anchor.example' } });
hub.on((e) => { if (e.t === 'delivered') render(e.deliveries); });
// Outbound DIDComm v2 forward (authcrypt when peer X25519 key is known, anoncrypt fallback):
await hub.sendForward('did:nostr:<recipient>', {
id: 'msg-1',
type: 'https://didcomm.org/basicmessage/2.0/message',
from: `did:nostr:${leafPubkey}`,
to: ['did:nostr:<recipient>'],
body: { content: 'hello' },
});
// NIP-77 Negentropy set reconciliation over the shared relay session:
const { have, need } = await hub.reconcile({ kinds: [11801] }, localItems);- One connection per origin. Every tab asks Web Locks for one lock; the
holder leads and alone runs the session, the others reach it over a
BroadcastChannel and take over when it closes (
src/pwa/hub.ts). A service worker cannot hold this connection: browsers stop an idle worker within about thirty seconds, open socket or not. - IndexedDB keeps the identity encrypted under a non-extractable
WebCrypto key, the grant and keylist, the mediator, and an inbox evicted by
age (
src/pwa/store.ts). - Foreground networking, push to wake. The leader drains on launch, when
the page becomes visible, and when the service worker posts
SW_PICKUPafter a Web Push. Anchors do not send pushes yet.
demo/ is a small installable page built on this. Run a real anchor and the
page side by side:
npm run demo:anchor # prints the link to open
npm run demoThe anchor leaves a numbered hello for every leaf it admits. Open a second tab: it follows; close the first: the second takes over.
