mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add kind:31234 as an author-only, channel-less, parameterized-replaceable event kind for encrypted draft wraps per NIP-37. Privacy enforcement spans every relay read path: - WS REQ: AUTHOR_ONLY_KINDS gate closes the subscription with restricted: for any requester who isn't the author - WS COUNT: same gate applied before the count query executes - HTTP bridge /query + /count: post-filter and guard use AUTHOR_ONLY_KINDS - Live fan-out: AUTHOR_ONLY_KINDS check in dispatch_persistent_event_inner prevents draft events from being pushed to non-author subscribers - FTS (NIP-50): migration 0007 sets search_tsv = NULL for kind:31234, making drafts storage-level unsearchable Ingest validation (validate_draft_wrap_envelope): - Exactly one non-empty d tag (any bounded value; relay is grammar-agnostic) - Exactly one k tag with canonical u16 decimal (no leading zeros, fits u16) - No h or p outer tags (compose context belongs in encrypted payload only) - Content: empty string (tombstone) or NIP-44 v2 ciphertext shape check - Optional expiration: at most one, decimal, strictly future, ≤ safe integer NIP-11 now advertises NIP-37. NIP-40 is intentionally not advertised because Buzz does not yet suppress expired rows on read. Schema migration 0007 extends the search_tsv generated column exclusion list with kind 31234. New tests: - 23 unit tests for validate_draft_wrap_envelope in ingest.rs covering every acceptance and rejection path - Comprehensive E2E test suite in e2e_nip37_draft.rs covering write validation, NIP-01 replacement ordering, tombstone persistence, author-only REQ/COUNT/HTTP, kindless/mixed filter privacy, known-d privacy tripwires, live fan-out isolation, and NIP-11 advertisement Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
38 lines
2.0 KiB
SQL
38 lines
2.0 KiB
SQL
-- Exclude kind 31234 (NIP-37 draft wraps) from full-text search.
|
|
--
|
|
-- NIP-37 draft wraps carry NIP-44-v2 ciphertext in `content` (or empty string
|
|
-- for deletion tombstones). Indexing ciphertext would waste storage and violate
|
|
-- the "channel/DM context lives only inside the encrypted payload" invariant
|
|
-- that makes draft wraps author-private. The relay also must not index
|
|
-- plaintext compose context through any search surface.
|
|
--
|
|
-- Additive migration: previously applied files must not change checksum.
|
|
-- We must DROP the generated column and re-ADD it with the extended exclusion
|
|
-- list; ALTER COLUMN cannot change a GENERATED expression in Postgres.
|
|
--
|
|
-- Final kind exclusion list after this migration:
|
|
-- 1059 = KIND_GIFT_WRAP (NIP-17 ciphertext)
|
|
-- 30300 = KIND_EVENT_REMINDER (AUTHOR_ONLY_KINDS — defense in depth)
|
|
-- 30622 = KIND_DM_VISIBILITY (per-viewer private hide state)
|
|
-- 31234 = KIND_DRAFT (NIP-37: AUTHOR_ONLY_KINDS — ciphertext or tombstone)
|
|
-- 44100 = KIND_MEMBER_ADDED_NOTIFICATION (p-gated membership notice)
|
|
-- 44101 = KIND_MEMBER_REMOVED_NOTIFICATION (p-gated membership notice)
|
|
-- 44200 = KIND_AGENT_TURN_METRIC (NIP-AM: p-gated encrypted turn metrics)
|
|
-- Constants kept in `buzz_core::kind`; inlined here because a sqlx migration
|
|
-- is frozen SQL and cannot import the Rust constant. If a new privacy-sensitive
|
|
-- kind is added there, add a new additive migration following this pattern and
|
|
-- add a regression test in `buzz-search/tests/fts_integration.rs`.
|
|
--
|
|
-- NULL tsvector never matches `@@`, so excluded rows are storage-level
|
|
-- unsearchable.
|
|
|
|
ALTER TABLE events DROP COLUMN search_tsv;
|
|
ALTER TABLE events ADD COLUMN search_tsv TSVECTOR GENERATED ALWAYS AS (
|
|
CASE WHEN kind IN (1059, 30300, 30622, 31234, 44100, 44101, 44200) THEN NULL::tsvector
|
|
ELSE to_tsvector('simple', content)
|
|
END
|
|
) STORED;
|
|
|
|
-- Recreate the GIN index dropped with the column.
|
|
CREATE INDEX idx_events_search_tsv ON events USING GIN (search_tsv);
|