# RelayFabric example configuration. Deny by default: nothing is bridged
# unless a route says so (spec §38). Validate with: switchyardd --check-config
node:
  name: example-gateway
  data_dir: /var/lib/relayfabric
  # Set to true to require all routes to be covered by public_services
  # ingress/egress; see public_services block below.
  # public: false

plugins:
  mqtt:
    enabled: true
    # Managed by switchyardd; omit `command` to run the plugin yourself
    # (it connects to <data_dir>/plugins.sock using RELAYFABRIC_SOCKET).
    command: relayfabric-mqtt
    config:
      broker: mqtt://127.0.0.1:1883
      topics: [chat/a, chat/b]
      # Secret references (design §2 / SPEC §51, §59): a STRING value inside
      # any plugin's config: block may be, in its ENTIRETY, `${env:NAME}` or
      # `${file:/abs/path}` -- no interpolation inside a longer string, the
      # whole value must be the reference. Resolved once at config load: an
      # env var (error if unset or empty) or a file's trimmed contents
      # (error if unreadable; the path MUST be absolute -- a relative
      # `${file:...}` is rejected). Resolved values are passed to the plugin
      # via the RELAYFABRIC_PLUGIN_CONFIG env var set at spawn (the plugin
      # scrubs it from its own environment right after parsing, so children
      # it spawns don't inherit it), but never appear in admin API
      # responses, logs, or --check-config output, which always shows the
      # unresolved ${...} form. Example (harmless here: this plugin has no
      # such field today):
      # broker_token: ${env:RELAYFABRIC_MQTT_TOKEN}

  # lxmf:
  #   enabled: true
  #   # Runs under sh -c, so it needs an absolute path and the venv's
  #   # python (a bare `relayfabric-lxmf` isn't on PATH and the script's
  #   # shebang won't see venv deps). venv must have run:
  #   #   pip install -r plugins/lxmf/requirements.txt
  #   command: /path/to/RelayFabric/.venv/bin/python /path/to/RelayFabric/plugins/lxmf/relayfabric-lxmf
  #   config:
  #     display_name: "RelayFabric Gateway"
  #     storage: /var/lib/relayfabric/lxmf
  #     rns_configdir: null          # null = default ~/.reticulum
  #     announce_interval: 3600
  #     stamp_cost: null             # set to require inbound proof-of-work
  #     propagation_node: "auto"     # "auto" | explicit dest hash hex | null
  #     max_attachment_bytes: 1000000  # per-attachment cap, applied both ways
  #     image_max_bytes: null        # null = falls back to max_attachment_bytes
  #     voice_to_codec2: null        # e.g. 1200 = transcode outbound voice to codec2
  #     channels:
  #       - name: pasadena
  #         members: ["a91d00aa..."] # lowercase LXMF destination hashes
  #         open: false              # closed: operator-managed membership only

  # signal:
  #   enabled: true
  #   # Runs under sh -c, so it needs an absolute path and the venv's
  #   # python (a bare `relayfabric-signal` isn't on PATH and the script's
  #   # shebang won't see venv deps). venv must have run:
  #   #   pip install -r plugins/signal/requirements.txt
  #   command: /path/to/RelayFabric/.venv/bin/python /path/to/RelayFabric/plugins/signal/relayfabric-signal
  #   config:
  #     account: "+1234567890"       # gateway's phone number
  #     rpc_url: http://127.0.0.1:7583  # signal-cli daemon URL
  #     groups:
  #       pasadena: "GRP=="          # channel: group_id (from listGroups)
  #     allowed_users: null          # null = all members; list UUIDs to restrict
  #     attachment_dir: ~/.local/share/signal-cli/attachments  # signal-cli's download dir
  #     max_attachment_bytes: 8000000  # per-attachment cap, applied both ways

  # meshtastic:
  #   enabled: true
  #   # Runs under sh -c, so it needs an absolute path and the venv's
  #   # python (a bare `relayfabric-meshtastic` isn't on PATH and the script's
  #   # shebang won't see venv deps). venv must have run:
  #   #   pip install -r plugins/meshtastic/requirements.txt
  #   command: /path/to/RelayFabric/.venv/bin/python /path/to/RelayFabric/plugins/meshtastic/relayfabric-meshtastic
  #   config:
  #     broker: mqtt://127.0.0.1:1883
  #     topic_root: msh/US              # your node's full root including region — verify with: mosquitto_sub -t 'msh/#' -v
  #     gateway_id: null                # null = accept all gateways; set to filter by hex ID
  #     max_text_bytes: 200             # daemon truncates upstream text
  #     channels:
  #       zone1: {index: 0, topic_channel: "general"}
  #       zone2: {index: 1, topic_channel: "tactics"}

  # meshcore:
  #   enabled: true
  #   # Runs under sh -c, so it needs an absolute path and the venv's
  #   # python (a bare `relayfabric-meshcore` isn't on PATH and the script's
  #   # shebang won't see venv deps). venv must have run:
  #   #   pip install -r plugins/meshcore/requirements.txt
  #   command: /path/to/RelayFabric/.venv/bin/python /path/to/RelayFabric/plugins/meshcore/relayfabric-meshcore
  #   config:
  #     connection: serial:///dev/ttyUSB0
  #     max_text_bytes: 160
  #     channels:
  #       primary: {index: 0}
  #       secondary: {index: 1}

  # nostr:
  #   enabled: true
  #   # Runs under sh -c, so it needs an absolute path and the venv's
  #   # python (a bare `relayfabric-nostr` isn't on PATH and the script's
  #   # shebang won't see venv deps). venv must have run:
  #   #   pip install -r plugins/nostr/requirements.txt
  #   command: /path/to/RelayFabric/.venv/bin/python /path/to/RelayFabric/plugins/nostr/relayfabric-nostr
  #   config:
  #     identity_file: /var/lib/relayfabric/nostr.nsec  # optional; generated if absent
  #     relays: ["wss://relay.example.com"]              # default relay set
  #     channels:
  #       regional:
  #         relays: ["wss://relay.example.com"]           # optional; falls back to default
  #         filter: {kinds: [1], "#t": ["pasadena"]}       # NIP-01 REQ filter
  #         publish_tags: [["t", "pasadena"]]              # tags on outbound events
  #     max_text_bytes: 280

  # bitchat:
  #   enabled: true
  #   # Runs under sh -c, so it needs an absolute path and the venv's
  #   # python (a bare `relayfabric-bitchat` isn't on PATH and the script's
  #   # shebang won't see venv deps). venv must have run:
  #   #   pip install -r plugins/bitchat/requirements.txt
  #   command: /path/to/RelayFabric/.venv/bin/python /path/to/RelayFabric/plugins/bitchat/relayfabric-bitchat
  #   config:
  #     identity_file: /var/lib/relayfabric/bitchat.nsec  # optional; generated if absent
  #     relays: ["wss://relay.example.com"]                # default relay set
  #     channels:
  #       pasadena:
  #         geohash: "9q5c"                                 # base32, Bitchat's alphabet
  #         relays: ["wss://relay.example.com"]              # optional; falls back to default
  #         nickname: "relayfabric"                          # optional; passthrough n-tag only
  #     max_text_bytes: 280

# Identity linking (opt-in, challenge-verified — docs/SPEC.md §19/§21/§22):
# a route's identity_mode: linked renders a VERIFIED link's display_name
# instead of the per-route pseudonym at egress; default is "pseudonymous"
# (no rendering change, and existing configs need no edits). Linking itself
# never happens implicitly — it's always an operator/user action via
# `switchyardctl link/unlink/identities` or the admin API's
# GET/POST/DELETE /v1/identities... endpoints, gated by a verification-code
# round-trip to the target's own device (requires a direct-capable plugin,
# e.g. lxmf).
routes:
  - name: demo
    sources: ["mqtt:chat/a", "mqtt:chat/b"]
    destinations: ["mqtt:chat/a", "mqtt:chat/b"]
    # identity_mode: linked
    # Rendering knobs (design §4): tuning for the transform pipeline that
    # already runs on every send. tag: "alias" (default) renders the
    # per-route pseudonym (or, in identity_mode: linked with a verified
    # link, that link's display_name); "none" suppresses the [tag] prefix
    # entirely, including the linked display_name. max_chars (0 = disabled,
    # else >= 16) truncates the message BODY to that many Unicode
    # characters -- the sender tag is not counted, so it's never shortened
    # by this even if it's a long linked display_name. The transport's own
    # byte cap still applies afterward to the whole message as the hard
    # floor, and unlike max_chars it MAY still truncate into the tag.
    # render:
    #   tag: alias
    #   max_chars: 900
    # security_mode (design §3, SPEC §113.1/§113.2, cycle H): "gateway"
    # (default) is today's transform/translate behavior above. "sealed"
    # AEAD-seals the payload end-to-end between federation gateways --
    # requires every destination to be a fed:<peer> peer with a
    # config-pinned sealed_key (see federation.peers[].sealed_key below);
    # a plaintext local plugin like mqtt above cannot be a sealed
    # destination. This route-level setting is best-effort only -- an
    # in-place edit back to "gateway" reloads cleanly; for a hard,
    # reload-enforced guarantee, set the node's privacy.minimum_security:
    # sealed below instead.
    # security_mode: gateway

  # - name: lxmf_mqtt_bridge
  #   sources: ["lxmf:pasadena", "mqtt:lxmf_bridge"]
  #   destinations: ["lxmf:pasadena", "mqtt:lxmf_bridge"]

policies:
  - name: small-payloads
    match:
      destination_protocol: [mqtt]
    rules:
      max_payload: 4096
      drop_kinds: [location]        # location never crosses by default (spec §74)
      attachments: reject           # mqtt is payload-constrained; drop attachments outright
      max_attachment_bytes: 500000  # alternative to `reject`: cap attachment size instead (bytes)

# Public service gating: routes and their ingress/egress protocols.
# Required to cover every route's protocols when node.public is true; optional when false.
# When node.public is true, every route's source protocols must be covered by
# the union of all public_services' ingress lists, and every route's destination
# protocols must be covered by the union of all public_services' egress lists.
# public_services:
#   - name: mqtt-gateway
#     type: mqtt
#     ingress: [mqtt]          # protocols accepted as route sources
#     egress: [mqtt]           # protocols accepted as route destinations
#   - name: bridge-to-lxmf
#     type: bridge
#     ingress: [mqtt]
#     egress: [lxmf]

# Rate limits and queue limits. All fields default to 0 (unlimited).
# limits:
#   per_sender:                # limits apply per unique message sender
#     messages_per_minute: 100
#     bytes_per_hour: 1000000
#   per_route:                 # limits apply per route
#     queue_max: 5000
#   global:                    # limits apply daemon-wide
#     queue_max: 100000
#     cas_max_bytes: 1000000000

# Transport protocol budgets: rate limits per egress protocol.
# Keys must be enabled plugin names. messages_per_minute of 0 is an error;
# omit the entry instead to have no limit.
# transport_budgets:
#   mqtt:
#     messages_per_minute: 500
#   lxmf:
#     messages_per_minute: 200

# Transport classes: the LINK a plugin's traffic actually rides, distinct
# from its protocol -- drives egress payload caps and media
# allow/disallow. Keys must be enabled plugin names. Absent entirely (as
# here) means every plugin gets a default class by protocol name (mqtt/
# signal/nostr/bitchat -> terrestrial_internet; meshtastic -> meshtastic;
# meshcore -> mesh_core; lxmf -> reticulum; anything else ->
# terrestrial_internet), which reproduces today's behavior exactly (no
# transport-level cap beyond the daemon's own 16 MiB frame limit, images
# and video allowed). An entry here overrides the class and/or individual
# policy fields on top of that class's built-in defaults; takes effect
# live, no daemon restart. At egress the composed policy caps the payload
# to min(plugin cap, transport cap), and an image/video attachment the
# class forbids is dropped and replaced with a body note (e.g. "[image
# 'photo.jpg' omitted -- constrained transport]") rather than sent --
# sealed routes are exempt (no transform ever touches a sealed payload).
# transports:
#   meshtastic: { class: meshtastic }                       # class -> its built-in default policy
#   mqtt:       { class: satellite_internet, max_payload_bytes: 32768, allow_images: false }

# Federation (switchyardd-to-switchyardd, cycle F — docs/SPEC.md §30/§31/
# §33/§85-87/§112.6-112.7): authenticated Noise links between daemons,
# signed envelopes with a gateway attestation chain, a persistent trust
# store, and route destinations that egress to peers
# (fed:<peer_name>/<remote_route>). Absent entirely (as here) = the whole
# feature is off; every v0.1/v0.2 config keeps loading unchanged.
# federation:
#   listen: "127.0.0.1:47000"        # optional; absent = outbound-only node
#   accept_from: verified            # verified|trusted
#   max_hops: 4                      # inbound envelopes at/over this are dead_lettered HOP_LIMIT
#   max_ttl_secs: 86400              # inbound TTL clamped down to this
#   identity_exposure: pseudonymous  # pseudonymous|full — outbound source ref handling
#   ingress_routes: [regional-chat]  # local routes federated peers may inject into (default: none)
#   peers:
#     - name: phoenix
#       node_id: "rf:<64hex>"
#       addr: "10.0.0.2:47000"
#       trust: trusted               # optional; default verified
#       messages_per_minute: 100     # optional; default 0 (unlimited) — aggregate egress cap for this peer's fed link
#       sealed_key: "<64hex>"        # optional; pins this peer's sealed-routing X25519 key (else advert-learned) — cycle H, §113.3
#   trusted: []                      # extra node_ids
#   blocked: []

# RFDP discovery (cycle G — docs/SPEC.md §111/§112.2): signed, expiring node
# advertisements exchanged over federation links (requires the federation
# block above), describing the services/protocols this node publishes via
# public_services. Absent entirely (as here) = disabled, matching every
# pre-cycle-G config.
# discovery:
#   mode: federation                 # disabled (default) | federation | public
#                                     # public requires node.public: true
#   advert_ttl_secs: 3600            # default 3600; min 300, max 86400

# Sealed-routing privacy floor (design §3, SPEC §113.2, cycle H): a
# node-level minimum a route's security_mode must meet, checked at
# --check-config (a route below the floor is rejected at load, never
# silently downgraded). Absent entirely (as here) = no floor, matching
# every pre-cycle-H config.
# privacy:
#   minimum_security: gateway        # gateway (default) | sealed
#   allow_gateway_decryption: true   # default true; false = this node refuses to be a
#                                     # sealed→plaintext termination point (ingress, §113.3) —
#                                     # this is the phase-1 downgrade-refusal enforcement point.
#   allow_protocol_downgrade: true   # default true; parsed/stored this cycle but NOT YET
#                                     # separately enforced — phase-1's actual downgrade-refusal
#                                     # gate is allow_gateway_decryption above (documented, not
#                                     # hidden: see docs/api-reference.md's security modes note)

# TTLs and limits (seconds)
ttl_default_secs: 86400
dedup_ttl_secs: 86400
hop_limit: 8
max_attachment_bytes: 8000000  # daemon-wide inbound attachment budget (bytes); 8 MiB is the default if omitted
