tenex-eventd NIP-46 bunker spec

tenex-eventd NIP-46 bunker spec

Goal

Turn tenex-eventd into a remote signer that keeps the secret in local operator custody while exposing a shareable bunker:// URI to remote clients. The URI, not the secret key, is what remote sessions receive.

Current design

  • Secret source: TENEX_EVENTD_SECRET_KEY_HEX / NOSTR_SECRET_KEY from secret.env.
  • Public identity metadata stays in nostr-identity.json.
  • Startup writes a local bunker://<pubkey>?relay=...&secret=... URI to ~/.config/tenex-eventd/bunker.uri with 0600 permissions.
  • The URI is intentionally local-only; it is not published to relays.

Transport

  • Incoming requests are kind 24133 events tagged with p=<bunker-pubkey>.
  • The daemon polls relays directly for bunker requests.
  • Responses are returned as kind 24133 events encrypted back to the client.
  • Relay set currently matches the tenex relay set:
    • wss://relay.damus.io
    • wss://nos.lol
    • wss://relay.primal.net

Read / write path

  • Read path tries Primal HTTP cache first for mentions/profile/thread reads.
  • Because Primal currently returns 500 for this identity, the daemon falls back to direct relay queries.
  • Write path signs locally and publishes directly to relays when alby_token is unset.

Supported bunker RPC methods

  • connect
  • get_public_key
  • sign_event
  • nip44_encrypt
  • nip44_decrypt
  • get_relays
  • ping

Permission model

  • connect requires the shared secret embedded in the local bunker URI.
  • sign_event is allowlisted to kinds 1 and 30023.
  • Initial intent is notes + long-form articles only.
  • Unsupported or disallowed methods return an error and are not signed.

Runtime behavior

  • Mention polling remains on the slower normal interval (poll_interval = 60).
  • Bunker traffic is handled on a faster dedicated cadence (bunker.poll_interval = 5).
  • This keeps remote-signing latency reasonable without forcing the whole daemon into a 5-second Primal loop.

Verification

Fact: local unit tests pass for connect/auth success, wrong-secret rejection, and disallowed-kind rejection.

Fact: live round-trip verification against the launchd daemon passed using the generated local bunker URI:

  1. connect
  2. get_public_key
  3. sign_event for kind 1
  4. nip44_encrypt
  5. nip44_decrypt
  6. rejection of disallowed kind 4

Known gaps

  • No persisted per-client ACLs yet; authorization is currently shared-secret based.
  • No session expiry / rotation policy yet beyond bunker URI regeneration.
  • If bunker.connect_secret is left empty, a new secret is generated on restart; copied old bunker URIs stop working.
  • No switch_relays flow yet.
  • No bunker-specific rate limiting yet.
  • The Primal 500 path is still degraded, so direct relay fallback is operationally required today.

Recommendation

Use this bunker mode as the immediate secure handoff layer. Remote sessions get a bunker:// URI, not an nsec. Keep the current scope narrow, then add ACL persistence, URI/secret lifecycle policy, and relay-management UX before treating this as general-purpose signing infrastructure.


No comments yet.