Metadata-Version: 2.4
Name: fleetmesh-strfry
Version: 0.2.0
Summary: The strfry relay bridge for a Fleetmesh node: in-process plugin or standalone service
License: CC0-1.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fleetmesh-node>=0.3.0
Dynamic: license-file

# fleetmesh-strfry

The strfry relay bridge for a Fleetmesh node, in its own repo. It was
`node/strfry_bridge.py` and `meshnode strfry` inside meshnode; the code is the
same, moved, with two ways to run it.

What it does:

- **StrfryRelayBridge** (`fleetmesh_strfry.bridge`): a WebSocket client that
  subscribes to a running [strfry](https://github.com/hoytech/strfry) relay,
  verifies and stores what arrives in the node's event store, and publishes
  the node's egress to strfry; egress that could not be sent is queued and
  drained on every tick / `flush_egress()`.
- **StrfryPolicyEngine / StrfryPluginRunner** (`fleetmesh_strfry.policy`): the
  strfry [write-policy plugin](https://github.com/hoytech/strfry/blob/master/docs/plugins.md)
  protocol over stdin/stdout - signature, id, clock-skew, NIP-40 expiry, size,
  kind/shard/pubkey lists, rate limits, and optional bilateral peering-grant
  enforcement against the node's `grants.sqlite` - plus the symmetric egress
  policy (drop / redact).
- **StrfryPipe** (`fleetmesh_strfry.pipe`): bulk JSONL export/import between
  the node store and `strfry export` / `strfry import`.

## Where it runs

Standard library only, but it imports the node package at runtime
(`node.protocols.nostr.eventstore`, `node.protocols.nostr.client`,
`node.storage.grants`, `node.keystore`, `node.interfaces`, `node.compartment`,
`node.commands.base`) and, through it, the
reference `fleetmesh_core` and the schema package `fleetmesh_schema`. It runs
**on the node host** with those packages importable:

```
pip install fleetmesh-strfry      # pulls in fleetmesh-node and, through it, core, schema, protocols
```

or, developing from checkouts, as sibling repos on `PYTHONPATH`:

```
/opt/fleetmesh/meshnode        # node/
/opt/fleetmesh/core            # fleetmesh_core/
/opt/fleetmesh/schema          # fleetmesh_schema/ (+ schema/, schemas/)
/opt/fleetmesh/strfry-bridge   # this repo
export PYTHONPATH=/opt/fleetmesh/strfry-bridge:/opt/fleetmesh/meshnode:/opt/fleetmesh/core:/opt/fleetmesh/schema
```

## Mode 1: in-process plugin

The node loads `StrfryBridgePlugin` as a compartment (meshnode
`docs/extending.md`): it starts and stops with the node, drains its egress
queue on every node tick, and reports under `health.subsystems.strfry` /
`meshnode status`. In the node's `config.json`:

```json
{
  "plugins": [{"ref": "fleetmesh_strfry.plugin:StrfryBridgePlugin", "after": "bridge"}],
  "extensions": {
    "strfry": {
      "url": "ws://127.0.0.1:7777",
      "policy": {"enforce_grants": false, "shadow_reject": false},
      "kinds": [0, 1, 3, 6, 7, 1059, 11801, 11802, 11803, 30801, 30802, 30803]
    }
  }
}
```

`after` is optional; `policy` takes every `StrfryPolicyConfig.from_dict` key
(`max_event_bytes`, `allowed_kinds`, `blocked_kinds`, `quarantine_kinds`,
`allowed_shards`, `blocked_pubkeys`, `rate_limit_per_min`, ...); `kinds` is the
strfry subscription filter. The plugin does not *require* the node's `bridge`
compartment: when the node has one it binds to it, when not it still runs.

With the plugin named in `config.json`, `meshnode` grows the verb back:

```
meshnode strfry DIR status|plugin|export|import|eval [--url] [--out] [--in] [--event] [--enforce-grants] [--shadow] [--json]
```

A `bridge.strfry` block left over from before the move is refused by the node's
config check with a message naming this plugin.

### Roster-protected writes: `roster_file`

A relay that serves a private network (kind 30830, see
[fleetmesh.org/networks.html](https://fleetmesh.org/networks.html)) must accept
writes only from the network's roster. Name the file a meshnode exports from
`face.roster_export_path` (shape `{"<network id>": ["<pubkey hex>", ...]}`):

```json
{
  "roster_file": "/var/lib/meshnode/roster.json",
  "allowed_pubkeys": ["<network creator pubkey hex>"]
}
```

Naming a `roster_file` makes the relay roster-only: every pubkey in the file is
merged with the static `allowed_pubkeys`, the file is re-read when its mtime
changes, and a missing or half-written file is *nobody*, not everybody.

Two facts of the current code, not design intent:

- It gates **writes only**: `EVENT` goes through the policy, `REQ` and `AUTH`
  do not. Read-side protection is strfry's own NIP-42 configuration.
- There is **no empty-roster exception** for the first 30830 (the
  `fleetmesh_core.network.relay_allow` rule has one; this plugin does not), so
  the network creator's pubkey must be in `allowed_pubkeys` for the first
  definition to land.

## Mode 2: standalone service

```
python -m fleetmesh_strfry run    --dir NODE_DIR [--url ws://127.0.0.1:7777] [--every 30]
python -m fleetmesh_strfry plugin --dir NODE_DIR            # strfry writePolicy stdio runner
python -m fleetmesh_strfry status|export|import|eval --dir NODE_DIR [flags]
```

`run` opens the node directory's `store.sqlite` / `grants.sqlite` the way
`meshnode strfry` did, runs a `StrfryRelayBridge` against `--url`, and
flushes the egress queue every `--every` seconds until SIGTERM/SIGINT. Exit
codes: 0 on a clean stop or an accepted `eval`, 1 when `eval` rejects, 2 when
the arguments are unusable (no such directory, no `keys.json`).

`contrib/fleetmesh-strfry.service` is the systemd unit:

```
install -m 644 contrib/fleetmesh-strfry.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now fleetmesh-strfry
```

Edit `NODE_DIR`, `STRFRY_URL` and `PYTHONPATH` in it to match the host.

For strfry's own write policy, point `strfry.conf` at the stdio runner.
`--policy FILE` takes a JSON object of `StrfryPolicyConfig.from_dict` keys
(`allowed_kinds`, `max_event_bytes`, `rate_limit_per_ip`, ...); `--no-ingest`
evaluates without copying accepted events into the node's `store.sqlite`, for a
relay where strfry is the only store:

```
relay {
    writePolicy {
        plugin = "/usr/bin/env PYTHONPATH=/opt/fleetmesh/strfry-bridge:/opt/fleetmesh/meshnode:/opt/fleetmesh/core:/opt/fleetmesh/schema python3 -m fleetmesh_strfry plugin --dir /var/lib/meshnode --policy /etc/fleetmesh/policy.json --no-ingest"
    }
}
```

## Tests

Scripts, as in meshnode; each prints `N passed, M failed` and exits non-zero on
a failure. With meshnode a sibling checkout:

```
PYTHONPATH=.:../meshnode:../core:../schema python3 tests/test_strfry_bridge.py   # policy, runner, pipe, live bridge, CLI
PYTHONPATH=.:../meshnode:../core:../schema python3 tests/test_plugin.py          # plugin lifecycle, stdio subprocess, service loop, a node with the plugin
```

CI (`.gitlab-ci.yml`) clones meshnode, core and schema beside the checkout and
runs the same.

## License

CC0-1.0, as meshnode (`LICENSE`).
