nostr_dict() returned every model field except relay_id and publisher,
so the size column added for storage accounting leaked into every
EVENT sent to clients. Emit the seven NIP-01 fields explicitly instead.
Caught by the client-flow test ported from upstream.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
A kind-5 request could only name events by `e` tag. Worse, one that
carried `a` tags and no `e` tags built a filter with an empty id list,
which matched every event by that author and marked them all deleted.
Handle `a` tags per NIP-09: parse `kind:pubkey:d` (the `d` value may
contain ':'), require the pubkey to be the request author and the kind
to be replaceable or addressable, and remove only versions up to the
request's `created_at` so a later re-publication survives. An empty
`d` addresses a replaceable kind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
(cherry picked from commit e695f9c66881d3d520d40f0cd3e9c95d9435938c)
notify_event returned after the first filter that matched, so a
connection holding several subscriptions only ever received an event on
one of them. That is invisible with one subscription per client, but a
multiplexer such as nostrclient funnels all of its clients through a
single connection. With nwcprovider subscribed to its own kind-23195
responses, every NWC reply was handed to that subscription and stopped
there; the wallet app's subscription on the same connection never saw
it, and Amethyst reported "wallet request timed out". Direct to the
relay it worked, because the app then had its own connection.
Deliver once per subscription id instead, and demote the per-filter
miss log to debug: it emitted one INFO line per filter per event.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
Generalize the AUTH-gated, recipient-only delivery rule from NIP-04 to
also cover NIP-17 kind 1059 gift wraps. When the relay is configured to
require AUTH for kind 1059, only the AUTH'd recipient named in the
event's `p` tag receives it; otherwise gift wraps broadcast like any
regular event.
- relay/event.py: add `is_seal`, `is_gift_wrap`, `is_private_message`
helpers (kinds 13, 1059)
- relay/client_connection.py: rename `_is_direct_message_for_other` ->
`_is_private_event_for_other`; key off `is_private_message` so the
same gating applies to kinds 4 and 1059
- relay/relay.py: advertise NIPs 17, 44, 59 in NIP-11 supported_nips
- README: document NIP-17/44/59 transport-level support
- tests/test_nip17.py: unit tests for kind classification, AUTH-gated
1059 delivery (recipient vs non-recipient vs unauthenticated), and
regression coverage for kind 4 gating
NIP-44 (encryption) and NIP-59 (wrap/seal) are client-side concerns;
the relay treats payloads as opaque ciphertext and stores kind 1059
like any regular event.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changes:
- relay/event.py: Add `size: int = 0` field to NostrEvent model
- relay/client_connection.py: Set `event.size = event.size_bytes` when creating events from WebSocket messages
The size field has existed in the database schema since migration m001 but was never populated, causing:
- Incorrect storage accounting (always 0)
- Broken storage quota enforcement
- Failed event pruning when storage limits reached
The size field is internal relay metadata and is excluded from the nostr_dict() output, maintaining NIP-01 compliance. The size_bytes property calculates the actual byte size of the event's JSON representation.
Fixes: Database constraint violation when inserting events without the required size column value.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix inverted logic in _can_add_filter() method that was preventing new
subscription filters from being added
- Fix REQ message handling to properly clear existing filters before
adding new ones
- Fix inverted condition check when validating filter addition
capacity
- Add debug logging to track filter matching and broadcast failures
These bugs were causing customer order events (NIP-15) to be received
by the relay but not
forwarded to nostrclient/nostrmarket, requiring server restarts or
manual refresh to process orders.
The fix ensures proper event propagation: Customer → Relay →
nostrclient → nostrmarket → Invoice.
Root cause: The _can_add_filter() method returned true when filters >=
max instead of when
filters < max, and the validation check used the wrong conditional,
effectively blocking all
new filter subscriptions after initial connection.
Fixed bug where deleting a parameterized replaceable event (e.g., kind 31922)
using an 'a' tag would incorrectly delete ALL events of that kind instead of
just the specific event with the matching d-tag.
**Root Cause:**
NostrFilter's 'd' field uses a Pydantic Field alias "#d". When creating a filter
with `NostrFilter(d=[value])`, Pydantic ignores it because the parameter name
doesn't match the alias.
**Fix:**
Changed filter creation to use the alias:
```python
NostrFilter(authors=[...], kinds=[...], **{"#d": [d_tag]})
```
**Testing:**
- Created two tasks with different d-tags
- Deleted only one task
- Verified only the specified task was marked as deleted in the database
- Confirmed the other task remained unaffected
This ensures proper NIP-09 deletion behavior for NIP-33 parameterized
replaceable events using 'a' tag format (kind:pubkey:d-identifier).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extended NIP-09 deletion event handling to support both regular events
and parameterized replaceable events (NIP-33).
**Previous behavior:**
- Only handled 'e' tags (regular event IDs)
- Did not support 'a' tags for addressable/replaceable events
**New behavior:**
- Handles both 'e' tags (event IDs) and 'a' tags (event addresses)
- Parses 'a' tag format: kind:pubkey:d-identifier
- Validates deletion author matches event address pubkey (NIP-09 requirement)
- Creates appropriate filters for each deletion type
**Implementation:**
- Added parsing for 'a' tag event addresses
- Extract kind, pubkey, and d-tag from address format
- Build NostrFilter with authors, kinds, and d-tag parameters
- Collect all event IDs to delete from both 'e' and 'a' tags
- Mark matching events as deleted in single operation
This enables proper deletion of parameterized replaceable events like
calendar events (kind 31922-31924), long-form content (kind 30023),
and other addressable event kinds.
Implements NIP-09: https://github.com/nostr-protocol/nips/blob/master/09.md
Supports NIP-33: https://github.com/nostr-protocol/nips/blob/master/33.md
* Implement NIP-16 parameterized replaceable events
Add support for parameterized replaceable events (kinds 30000-39999) to
properly
handle Nostr marketplace product and stall updates according to NIP-16
specification.
Changes:
- Add is_parameterized_replaceable_event property to NostrEvent
- Implement automatic deletion of previous versions when new
parameterized replaceable event is received
- Add 'd' tag filtering support to NostrFilter for parameterized
replacement logic
- Update SQL query generation to handle 'd' tag joins
Fixes issue where product updates would create duplicate entries instead
of
replacing previous versions, ensuring only the latest version remains
visible.
* Refactor event handling for addressable events
Renamed the property is_parameterized_replaceable_event to is_addressable_event in NostrEvent to align with NIP-01 specifications (previously NIP-16). Updated the client_connection.py to utilize the new property for extracting 'd' tag values for addressable replacement, ensuring proper event handling in the relay system.
* Refactor tag filtering logic in NostrFilter
Updated the tag filtering mechanism to ensure that the filter only fails if the specified tags ('e' and 'p') are not found. This change improves clarity and maintains functionality by allowing for more precise control over event filtering.
* update readme
* Fix addressable event deletion and SQL schema issues
- Fix Pydantic field alias usage for d tag filtering (use #d instead of
d)
- Remove nostrrelay schema prefixes from SQL table references
- Implement subquery approach for DELETE operations with JOINs
- Resolve SQLite DELETE syntax incompatibility with JOIN statements
- Ensure NIP-33 compliance: only delete events with matching d tag
values
This fix addresses an issue where REQ messages with multiple filters
were being rejected by the relay. Notably: The nostrmarket extension's
"Refresh from Nostr" functionality sends a single REQ message containing
4 different filter subscriptions:
- Direct Messages (kinds: [4])
- Stalls (kinds: [30017])
- Products (kinds: [30018])
- Profile (kinds: [0])
Changes:
- Changed validation from `len(data) != 3` to `len(data) < 3` to allow
multiple filters
- Added loop to process all filters in a single REQ message (data[2:])
- Accumulate responses from all filters before returning
This ensures compatibility with clients that batch multiple subscription
filters in a single REQ message, which is a valid pattern according to
NIP-01.