Skip to content

feat(push): add a UnifiedPush transport profile to the NIP-PL push gateway (Google-free Android, self-host friendly) #7482

Description

@ZeroOttantotto

Summary

Add an optional UnifiedPush transport profile to buzz-push-gateway, so Android clients can receive NIP-PL wakes without any Google dependency and self-hosted relay operators can deliver wakes through a distributor they own (e.g. a self-hosted ntfy instance).

This mirrors #3229 (FCM transport profile): a new transport on the existing NIP-PL foundation — same lease lifecycle, executor key custody, replay protection, quota, and delivery-authority model — rather than a downstream push architecture.

Motivation

NIP-PL already reserves UnifiedPush as an optional transport ("transport": "apns" | "fcm" | "unifiedpush") and states exactly what a future profile must add:

A future profile requires a separately registered constant body and hostile-endpoint analysis.

Everything else is already transport-agnostic: the relay-side matcher (crates/buzz-relay/src/push_runtime.rs) matches leases and wakes without caring about the transport, and the gateway already abstracts senders behind the PushTransport trait (crates/buzz-push-gateway/src/apns.rs).

UnifiedPush is the only Android transport that keeps a self-hosted Buzz deployment fully Google-free, and it composes naturally with the wake-only model: the distributor delivers a fixed body, the client reconnects and fetches authoritative events over normal REQ. No event content, no relay-supplied bytes ever transit the transport — the same noninterference property the APNs profile enforces with its byte-exact APNs body.

Proposed profile

Registered constant body

Analogous to the APNs constant, define one gateway-owned UTF-8 constant that the gateway sends for every accepted UnifiedPush attempt, e.g.:

{"v":1,"t":"reconnect"}

The exact bytes are maintainers' choice — the requirement is a single fixed constant, never containing relay-derived, lease-derived, or request-derived data. The distributor forwards it as-is to the client's UnifiedPush receiver; the client treats any wake the same way it treats an APNs wake: reconnect and REQ.

Hostile-endpoint analysis

The UnifiedPush endpoint is a full HTTPS URL chosen by the distributor (typically https://<ntfy-host>/up?up=...), which is the core of the NIP's v1 objection. Proposed mitigations:

  • No sensitive data at risk: the gateway POSTs only the registered constant body. No lease payload, no tokens, no NIP-98 auth headers toward the endpoint (contrast with relay→gateway auth). A hostile endpoint learns nothing except "some Buzz installation woke".
  • Scheme pinning: https only; redirects not followed.
  • Optional per-profile endpoint allowlist: public-gateway deployments keep the profile disabled (or pin it to their own distributor); self-hosted operators configure exactly their ntfy base URL. This also contains SSRF exposure from the gateway process.
  • Reuse existing attempt/retry classification and max_endpoint_len bounds (4096 is ample for UP endpoints).

Gateway and relay surface

  • crates/buzz-push-gateway: new unifiedpush.rs implementing the existing PushTransport trait (mirroring apns.rs), route /v1/deliveries/unifiedpush, new AppProfile (e.g. buzz-android-up).
  • crates/buzz-relay/src/nip11.rs: the default descriptor currently hard-codes a single APNs profile ("app_profiles": [{"id": "buzz-ios-dogfood", "transport": "apns"}]); make app_profiles relay-configurable so self-hosted operators can advertise their UP profile, and extend class_support for the UP profile's classes.
  • Lease validation (crates/buzz-relay/src/handlers/push_lease.rs) already matches app_profile against the advertised descriptor and checks transport equality, so no schema change is needed there.

Client-side work (Android Flutter app: unified_push plugin, distributor handshake, publishing a kind:30350 lease with the UP endpoint) belongs to the Android umbrella in #6092; this issue covers only the gateway/NIP-PL profile.

Opt-in scoping

Keep the profile inert unless the relay advertises a UP-capable app_profile: public-gateway behavior for existing iOS profiles stays byte-identical, while self-hosted relays ("a relay you own") get Google-free Android push immediately.

Alternatives considered

Questions

  • Would a PR that keeps the profile disabled until app_profiles advertises a UP profile be acceptable?
  • Preference on the registered constant body bytes and the AppProfile id naming (buzz-android-up)?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions