~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-webrtc

WebRTC transport for atlibs: com.fedfork.exec.webrtc.connect in the browser, and webrtc-peer, its guest side

0
0
main
121 lines · 5.8 KB Raw History
# atlibs-webrtc

The WebRTC transport for [atlibs](https://knotless.dev/johnandersen777.bsky.social/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 dependencies
- `src/`: `webrtc-peer` (str0m), a crate on `atlibs-host`
- `web/loopback.html`: a page that drives the transport against a local `webrtc-peer`
- `tests/`: 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:

```bash
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs ../atlibs && git -C ../atlibs checkout -q 88b2ba8
```

## Build

```bash
cargo build --release
```

## Run a guest

Forward the `ssh` datachannel to a local sshd, and let one page origin signal it:

```bash
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`:

```html
<script type="importmap">
{ "imports": { "@publicdomainrelay/atlibs-webrtc/": "./webrtc/", "@publicdomainrelay/atlibs/": "./atlibs/" } }
</script>
```

Then route the calls to the transport, as `web/loopback.html` does:

```js
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.

```bash
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.

```toml
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.