Skip to content

feat(relay): implement NIP-ER event reminder support (kind:30300)#934

Draft
wpfleger96 wants to merge 4 commits into
mainfrom
paul/nip-er-relay-core
Draft

feat(relay): implement NIP-ER event reminder support (kind:30300)#934
wpfleger96 wants to merge 4 commits into
mainfrom
paul/nip-er-relay-core

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Stack

Stack: this PR → #957#963

This is the foundation PR. #957 (push scheduler) stacks on top of this branch.


Summary

Adds relay-side support for NIP-ER encrypted event reminders (kind:30300). This is a parameterized replaceable, author-only event type that carries a public not_before schedule tag while keeping reminder content NIP-44 encrypted.

Changes

Write path (sprout-core, sprout-relay/handlers/ingest)

  • Register KIND_EVENT_REMINDER (30300) in sprout-core::kind as parameterized replaceable, global-only
  • Add AUTHOR_ONLY_KINDS array for cross-cutting read-path enforcement
  • Validate not_before tag: at most one permitted, decimal digits only, no leading zeros (except "0"), range 0..=9007199254740991 (Number.MAX_SAFE_INTEGER). Optional — terminal-state and bookmark reminders omit it
  • Validate d tag: exactly one required, non-empty
  • Reject events where expiration <= not_before (window would expire before due)
  • Scope to UsersWrite — same as contacts, read-state, engrams

Read path (sprout-relay/handlers/req, count, api/bridge)

  • author_only_filters_authorized(): pre-filter gate rejects exclusive kind:30300 filters unless authors=[self] — returns CLOSED restricted: (WS) or HTTP 403
  • is_author_only_event(): per-event filter silently omits other authors' reminders from mixed-kind result sets
  • filter_can_match_author_only_kinds(): forces COUNT handler onto the slow path (per-event filtering) to prevent leaking aggregate counts
  • Applied consistently across WS REQ, WS COUNT, HTTP /query, HTTP /count, and HTTP /query search paths

Integration tests (e2e_event_reminder.rs)

16 e2e tests covering:

  • Write validation: valid reminders, missing/duplicate/malformed not_before, d-tag validation, expiration ordering, edge cases (0, MAX_SAFE_INTEGER)
  • Read filtering: author self-query succeeds, cross-user query rejected (HTTP 403 + WS CLOSED), mixed-kind filters omit non-author reminders, parameterized replacement semantics

@wpfleger96 wpfleger96 requested a review from a team as a code owner June 9, 2026 23:09
@wpfleger96 wpfleger96 force-pushed the paul/nip-er-relay-core branch from 90eda63 to 512d676 Compare June 9, 2026 23:10
@wpfleger96 wpfleger96 marked this pull request as draft June 10, 2026 00:00
@wpfleger96 wpfleger96 force-pushed the paul/nip-er-relay-core branch 6 times, most recently from 6503871 to 7a75e4a Compare June 10, 2026 18:33
@wpfleger96 wpfleger96 force-pushed the paul/nip-er-relay-core branch from 7a75e4a to 84b0dad Compare June 11, 2026 19:21
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 4 commits June 15, 2026 23:00
Adds relay-side support for NIP-ER encrypted event reminders:

Write path (ingest):
- Register KIND_EVENT_REMINDER (30300) as parameterized replaceable,
  global-only, and UsersWrite-scoped
- Validate not_before tag: exactly one, decimal digits only, no leading
  zeros, range 0..=MAX_SAFE_INTEGER
- Reject expiration <= not_before when both are present and parseable

Read path (REQ/COUNT/bridge):
- AUTHOR_ONLY_KINDS array gates visibility — only the authenticated
  event author may read kind:30300 events
- Exclusive author-only filters (kinds:[30300]) require authors=[self]
  or get CLOSED/403
- Mixed-kind filters pass the gate but per-event filtering silently
  omits other authors' reminders
- COUNT fast path disabled when author-only kinds are in play to
  prevent leaking aggregate counts

Integration tests (e2e_event_reminder.rs):
- Write validation: valid, missing, duplicate, malformed not_before;
  expiration ordering; edge cases (0, MAX_SAFE_INTEGER)
- Read filtering: author self-query, cross-user rejection (HTTP 403
  and WS CLOSED), mixed-kind omission, replacement semantics

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Will Pfleger <wpfleger@squareup.com>
Signed-off-by: Will Pfleger <wpfleger@squareup.com>
The spec requires not_before on pending reminders only — terminal states
(done/cancelled) and bookmarks omit it. The validator incorrectly
required exactly one not_before on all kind:30300 events, blocking the
entire completion/cancellation lifecycle.

Also adds d-tag structure validation per spec: reject zero, empty, or
duplicate d tags.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The author-only delivery gate for NIP-ER reminders (AUTHOR_ONLY_KINDS)
lived inline in dispatch_persistent_event, so it covered only the
in-process fan-out path. The Redis cross-node path (subscribe_local in
main.rs) calls fan_out + filter_fanout_by_access but never applied the
author-only check — a reminder published on one node and received by
another was fanned out to any matching subscriber, leaking author-private
reminders across nodes.

Move the gate into filter_fanout_by_access, the single post-fan_out
chokepoint both delivery paths already share. It now runs independent of
channel scope (author-only kinds are stored globally, channel_id=None) and
cannot be bypassed by any path that delivers through the shared filter.
The redundant inline check is removed.

Document the Redis routing trust boundary at publish_event: per-author
routing is not the isolation boundary (every node PSUBSCRIBEs
buzz:channel:* and must receive every event since the author may connect
to any node); filter_fanout_by_access is the actual author-only delivery
boundary, now enforced on both fan-out paths.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the paul/nip-er-relay-core branch from fc76112 to c93ec38 Compare June 16, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant