atlibs-webrtc
The WebRTC transport for atlibs
(knotless://johnandersen777.bsky.social/atlibs): com.fedfork.exec.webrtc.connect in a browser page.
WebrtcHost finds a guest's offer, answers, opens a datachannel and can run ssh over it. It also fetches
webrtc: URLs: HTTP/1.1 to a guest's tunnel tag over datachannels (the guest agent's MCP server is
webrtc:///<ref>/shell/mcp). Signalling goes through the module (sealed with age); connectivity is STUN only.
webrtc-peer is the guest side on your machine, for local checks: it answers sealed offers and forwards
each datachannel label to a TCP port.
Layout
js/: the transport for pages (WebrtcHost,localSignals,httpChannel: HTTP/1.1 over a datachannel), no dependenciessrc/:webrtc-peer(str0m), a crate onatlibs-hostweb/loopback.html: a page that drives the transport against a localwebrtc-peertests/: the loopback check;lexicons/:com.fedfork.exec.webrtc.connect
Get the core
The crate takes atlibs-host as a git dependency (pinned by Cargo.lock). The page and the tests import
the core's JavaScript from a sibling checkout:
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs ../atlibs && git -C ../atlibs checkout -q 88b2ba8
Build
cargo build --release
Run a guest
Forward the ssh datachannel to a local sshd, and let one page origin signal it:
target/release/webrtc-peer --tunnel ssh=22 --origin http://127.0.0.1:8000
It prints {signal, fingerprint, ...}: open web/loopback.html?signal=<signal>&user=$USER from that origin.
Use it from a page
npm install @publicdomainrelay/atlibs-webrtc @publicdomainrelay/atlibs (Node >= 25), or map both in the
page's import map: @publicdomainrelay/atlibs-webrtc/ to js/ (here, web/webrtc is a symlink to
../js) and @publicdomainrelay/atlibs/ to the core's javascript/src:
<script type="importmap">
{ "imports": { "@publicdomainrelay/atlibs-webrtc/": "./webrtc/", "@publicdomainrelay/atlibs/": "./atlibs/" } }
</script>
Then route the calls to the transport, as web/loopback.html does:
import { WEBRTC_STATE, WebrtcHost, localSignals } from "@publicdomainrelay/atlibs-webrtc/index.js";
const host = new WebrtcHost({ store, events, signal, makePeer: (c) => new RTCPeerConnection(c), age, ssh });
const call = hostCall({ ...rest, calls: host.calls }); // webrtc.connect
await runModule({ ...rest, fetchers: { ...host.fetchers() } }); // webrtc: URLs for com.fedfork.exec.http.fetch
events({receipt, tag}) returns the guest events of that receipt and tag: a fetch peer is its own peer
connection, beside the page's. fetchers(options) is new WebrtcHost(options).fetchers() for a page
that only fetches (store is optional).
Mesh peers: recordSignals
recordSignals({repo, subhandle}) (js/record-signals.js) is {events, signal} for WebrtcHost over exact-key
records instead of a requester: the receipt is a peer's computer.socialweb.peer/<subhandle> strong ref
(peerReceipt(repo, did, subhandle)); its webrtc DID's computer.socialweb.ingress.listen/<subhandle> gives
the age key and DTLS fingerprint; the sealed offer goes to computer.socialweb.ingress.offer/<that DID>.<tid> in
the page's repo and the answer comes back at computer.socialweb.ingress.answer/<page DID>.<tid>. repo is the
page's PDS access (did, getRecord, putRecord, deleteRecord). meshChannel(host, receipt, {channel})
then speaks atirc's mesh channel lines over the atirc/mesh/1 datachannel; atirc's
tests/webrtc_loopback.ts drives it in headless Chrome against atirc mesh run.
HTTP to a bidder: endpoint URLs
A kcp bidder's offering lists {kind: "webrtc", address}, address = base64url of {"did", "listen"}.
webrtc://<address>/<path...> (endpointUrl({did, listen}, path)) is HTTP/1.1 to that bidder's market XRPC
over a market datachannel, signalled with new WebrtcHost({records: recordSignals({repo, subhandle}), ...}):
the receipt is its computer.socialweb.ingress.listen/<listen> uri, so the answerer is that DID itself. atlibs
hosts send module calls with endpoints there with service auth aud <did>#pdr_temp_market.
HTTP to a guest: webrtc: URLs
webrtc:///<ref>/<tag>/<path...>, ref = base64url of the JSON {"receipt": {"uri", "cid"}, "bidder"?}
(webrtcUrl({receipt, bidder}, tag, path) builds one, parseWebrtcUrl reads it). The fetcher is
async ({method, url, headers, body, signal}) => ({status, headers, body}) (body text). A module reaches
it through com.fedfork.exec.http.fetch with webrtc://* in its http list and the host's allowlist.
Each (receipt, tag) gets one peer connection, kept for later fetches and dialled again once it fails, and
a pool of datachannels, each one HTTP/1.1 connection serving one request at a time (keep-alive; a
Connection: close response retires it): a request in flight holds its channel, so concurrent requests
(a long-poll read beside an exec) each open or reuse their own. MCP session headers are the client's
(Mcp-Session-Id rides through like any header). host.close() closes them all.
Test
tests/loopback.test.mjs runs web/loopback.html in headless Chrome against webrtc-peer in front of a
throwaway sshd and a stub MCP server. It skips (with the reason) without Chrome (CHROME=<binary>), OpenSSH or the build.
cargo build --release && cargo test --release
node --test js/*.test.mjs tests/*.test.mjs
Publish
js/ is @publicdomainrelay/atlibs-webrtc on npm (npm publish --access public; the tests stay out of
the tarball). The crate is not on crates.io: depend on it by git.
atlibs-webrtc = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs-webrtc" }
STUN only. There is no TURN relay: two peers behind symmetric NATs will not connect.
