{"id":"1ecc2df209c0952157272afe5f570e15351030085b84816adbfb96708012518f","pubkey":"da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd","created_at":1775897205,"kind":30817,"tags":[["d","nip-dominion"],["title","NIP-DOMINION: Epoch-Based Encrypted Content Access"],["k","30480"]],"content":"NIP-XX\n======\n\nEpoch-Based Encrypted Content Access (Dominion)\n------------------------------------------------\n\n`draft` `optional`\n\nAuthors: [decented](https://github.com/decented)\n\nThis NIP defines a mechanism for encrypting Nostr content with epoch-based Content Keys (CKs) and distributing those keys to tiered audiences via gift-wrapped events. It enables revocable, scalable content access control on standard Nostr relays without custom relay software or new cryptographic primitives.\n\n## Motivation\n\nNostr content is either public or NIP-44 encrypted to specific recipients. There is no native mechanism for:\n\n- **Audience tiers** — different groups seeing different content (family, close friends, subscribers)\n- **Revocable access** — removing a recipient's ability to decrypt future content\n- **Scalable encryption** — encrypting to hundreds of recipients without per-recipient encryption operations\n\nPer-recipient NIP-44 encryption works for DMs but does not scale:\n\n| Recipients | NIP-44 approach | This NIP |\n|-----------|----------------|----------|\n| 1 | 1 encryption | 1 encryption + 1 key share |\n| 10 | 10 encryptions | 1 encryption + 10 key shares |\n| 100 | 100 encryptions | 1 encryption + 100 key shares |\n| 1,000 | 1,000 encryptions | 1 encryption + 1,000 key shares |\n\nContent is encrypted once with an epoch-based Content Key. Only the lightweight key distribution scales with audience size.\n\n## Overview\n\n```mermaid\nsequenceDiagram\n    participant A as Author\n    participant R as Relay\n    participant Rec as Recipient\n\n    note over A: Current Epoch\n    A->>A: Derive epoch CK from private key (HKDF)\n    A->>A: Encrypt content with CK (AES-GCM)\n    A->>R: Publish encrypted event\n    A->>Rec: Gift-wrap CK to each recipient (NIP-59)\n    Rec->>Rec: Unwrap gift, obtain CK\n    Rec->>R: Fetch encrypted event\n    Rec->>Rec: Decrypt content with CK\n\n    note over A: Next Epoch\n    A->>A: Derive new CK from private key\n    A->>Rec: Distribute new CK to current recipients\n    note over A: Revoked recipients are skipped\n```\n\n### Terminology\n\n| Term | Definition |\n|------|-----------|\n| **Content Key (CK)** | A 256-bit AES key used to encrypt content for a specific epoch and tier |\n| **Epoch** | A time period (default: one ISO 8601 week) during which a single CK is used |\n| **Epoch ID** | Identifier for the epoch, ISO 8601 string. Daily: `YYYY-MM-DD`, Weekly: `YYYY-Www`, Monthly: `YYYY-MM` |\n| **Vault share** | A gift-wrapped event containing a CK for a specific recipient |\n| **Tier** | An audience level (e.g. `family`, `connections`) that determines CK distribution |\n| **Vault config** | A self-encrypted NIP-78 event storing the author's tier memberships and settings |\n\n## Content Key Derivation\n\nCKs are derived deterministically using HKDF-SHA256:\n\n```\nCK = HKDF-SHA256(\n    ikm  = author's 32-byte private key,\n    salt = \"dominion-ck-v1\",\n    info = \"epoch:{epoch_id}:tier:{tier_name}\",\n    len  = 32\n)\n```\n\nThe salt string `dominion-ck-v1` is a fixed protocol constant. Implementations MUST use this exact value to ensure interoperability.\n\nThe `info` string includes both epoch ID and tier name, ensuring that each epoch/tier combination produces a unique key. Authors can always re-derive any CK from their private key material — no key database is needed.\n\n### Epoch ID Format\n\nEpoch IDs are ISO 8601 strings whose format depends on the configured length:\n\n| Length | Format | Example | Meaning |\n|--------|--------|---------|---------|\n| Daily | `YYYY-MM-DD` | `2026-04-13` | 13 April 2026 |\n| Weekly | `YYYY-Www` | `2026-W15` | Week 15 of 2026 (6–12 Apr) |\n| Monthly | `YYYY-MM` | `2026-04` | April 2026 |\n\nThe three formats are visually distinct (the `W` prefix disambiguates weekly from monthly) and each maps to a single calendar period in UTC. Implementations SHOULD default to weekly epochs and MAY support daily or monthly per tier. Daily epochs minimise the forward-only revocation window (24h exposure); monthly epochs reduce key-rotation overhead for slow-moving tiers.\n\n## Content Encryption\n\nAll content MUST be encrypted with AES-256-GCM using the epoch CK:\n\n```\ncontent = base64(iv || ciphertext || tag)\n```\n\n| Component | Size | Notes |\n|-----------|------|-------|\n| IV | 12 bytes | Random, unique per encryption |\n| Ciphertext | Variable | AES-GCM encrypted content |\n| Tag | 16 bytes | Authentication tag |\n\nEncrypted events MUST include a `vault` tag:\n\n```json\n[\"vault\", \"<epoch_id>\", \"<tier>\"]\n```\n\nThis tells recipients which CK to use for decryption. Both epoch ID and tier are REQUIRED since CKs are derived per-epoch and per-tier.\n\nExample encrypted event:\n\n```json\n{\n  \"kind\": 1,\n  \"pubkey\": \"<author_pubkey>\",\n  \"tags\": [\n    [\"vault\", \"2026-W10\", \"family\"]\n  ],\n  \"content\": \"<base64(iv || ciphertext || tag)>\"\n}\n```\n\nThe `vault` tag MAY be applied to any event kind. The kind of the event determines its semantics; the `vault` tag signals that the `content` field is Dominion-encrypted.\n\n### Tier Name Visibility\n\nThe tier name in the `vault` tag is visible to relay operators. For privacy-sensitive deployments, implementations MAY use opaque tier identifiers (e.g. hashed or random strings) instead of human-readable names. The protocol treats tier names as opaque strings.\n\n## Key Distribution\n\n### Kind 30480 — Vault Share\n\nA parameterised replaceable event containing an epoch CK for a specific recipient. This event MUST be NIP-44 encrypted to the recipient's pubkey, sealed in a kind 13 event, and gift-wrapped in a kind 1059 event (NIP-59) before publishing.\n\nInner event (before gift-wrapping):\n\n```json\n{\n  \"kind\": 30480,\n  \"pubkey\": \"<author_pubkey>\",\n  \"created_at\": 1709000000,\n  \"tags\": [\n    [\"d\", \"2026-W10:family\"],\n    [\"p\", \"<recipient_pubkey>\"],\n    [\"tier\", \"family\"],\n    [\"algo\", \"secp256k1\"],\n    [\"L\", \"dominion\"],\n    [\"l\", \"share\", \"dominion\"]\n  ],\n  \"content\": \"<hex-encoded 32-byte CK>\"\n}\n```\n\n**Tags:**\n\n| Tag | Status | Description |\n|-----|--------|-------------|\n| `d` | REQUIRED | `{epoch_id}:{tier}` — parameterised replaceable identifier |\n| `p` | REQUIRED | Recipient pubkey |\n| `tier` | REQUIRED | Audience tier name |\n| `algo` | REQUIRED | Asymmetric algorithm used (`secp256k1`) |\n| `L` | RECOMMENDED | Protocol namespace label (`dominion`) |\n| `l` | RECOMMENDED | Protocol label (`share`, namespaced under `dominion`) |\n\nThe `content` field contains the CK as a 64-character lowercase hex string.\n\n**Why a dedicated kind?** The kind 30480 event is always gift-wrapped (kind 1059) on the wire, so relays never see it directly. However, a registered kind is needed because:\n\n1. **Parameterised replaceability** — the `d` tag (`epoch:tier`) enables newer vault shares to replace older ones for the same epoch/tier/recipient, preventing stale key accumulation\n2. **Client-side filtering** — after unwrapping, clients need to distinguish vault shares from other gift-wrapped content (DMs, sealed events) by kind number\n3. **Algorithm tagging** — the `algo` tag on kind 30480 enables future migration to post-quantum algorithms without breaking backward compatibility\n\n**Example REQ filter (after gift-wrap unwrapping):**\n\n```json\n[\"REQ\", \"vault-shares\", {\"kinds\": [30480], \"authors\": [\"<author_pubkey>\"], \"#d\": [\"2026-W10:family\"]}]\n```\n\n### Grant Scope\n\nA grant distributes the current epoch's CK only. Implementations MUST NOT send historical epoch keys unless the author explicitly requests it. An accidental grant exposes at most one epoch of content.\n\n### Epoch Rotation\n\nWhen a new epoch begins, the author's client SHOULD:\n\n1. Derive the new epoch CK\n2. Distribute to all current tier members and individual grantees\n3. Skip revoked pubkeys\n\n## Vault Configuration\n\n### NIP-78 (Kind 30078) — Vault Config\n\nA NIP-78 app-specific data event storing the author's vault settings. The `content` field MUST be NIP-44 self-encrypted (to the author's own pubkey) before publishing.\n\n```json\n{\n  \"kind\": 30078,\n  \"pubkey\": \"<author_pubkey>\",\n  \"tags\": [\n    [\"d\", \"dominion:vault-config\"],\n    [\"encrypted\", \"nip44\"],\n    [\"algo\", \"secp256k1\"],\n    [\"L\", \"dominion\"],\n    [\"l\", \"config\", \"dominion\"]\n  ],\n  \"content\": \"<NIP-44 self-encrypted JSON>\"\n}\n```\n\n**Decrypted payload:**\n\n```json\n{\n  \"tiers\": {\n    \"family\": [\"<pubkey1>\", \"<pubkey2>\"],\n    \"connections\": \"auto\",\n    \"close_friends\": [\"<pubkey3>\"]\n  },\n  \"individualGrants\": [\n    {\n      \"pubkey\": \"<pubkey5>\",\n      \"label\": \"Tutor\",\n      \"grantedAt\": 1709000000\n    }\n  ],\n  \"revokedPubkeys\": [\"<pubkey6>\"],\n  \"epochConfig\": {\n    \"family\": \"monthly\",\n    \"connections\": \"weekly\"\n  }\n}\n```\n\n**Fields:**\n\n| Field | Type | Status | Description |\n|-------|------|--------|-------------|\n| `tiers` | Object | REQUIRED | Maps tier names to member pubkey arrays. `\"auto\"` indicates the tier is derived from the author's follow list (kind 3 contacts). |\n| `individualGrants` | Array | REQUIRED | One-off grants to specific pubkeys, independent of tiers. |\n| `revokedPubkeys` | Array | REQUIRED | Pubkeys to skip during CK distribution. |\n| `epochConfig` | Object | OPTIONAL | Per-tier epoch length. Values: `\"daily\"`, `\"weekly\"`, `\"monthly\"`. |\n\nTier memberships are private — stored as self-encrypted data, not published via NIP-51 lists. This protects the author's social graph.\n\n## Audience Tiers\n\n| Tier | Who receives CK | Distribution |\n|------|-----------------|-------------|\n| Public | Everyone | No encryption needed — standard plaintext event |\n| Connections | Mutual follows or curated list | Auto-distribute on epoch rotation |\n| Close friends | Curated list | Auto-distribute on epoch rotation |\n| Family | Explicitly managed list | Auto-distribute on epoch rotation |\n| Private | Self only | No distribution — author-only |\n\nImplementations MAY define custom tier names. The protocol treats tier names as opaque strings.\n\n### Individual Grants\n\nIndependent of tiers. An author MAY grant CK access to any specific pubkey via `individualGrants` in the vault config.\n\n## Revocation\n\nRevocation is forward-only: stop distributing CKs for new epochs to the revoked recipient.\n\n| Epoch length | Max exposure after revocation |\n|-------------|-------------------------------|\n| Daily | 24 hours |\n| Weekly | 7 days |\n| Monthly | 30 days |\n\nThe revoked recipient retains any CKs they already received. Content from those epochs remains accessible. This is the same model used by Signal, WhatsApp, and Matrix for group key management.\n\nRevoked pubkeys are tracked in the vault config. During epoch rotation, the distribution loop MUST skip any pubkey in `revokedPubkeys`.\n\n## Metadata Privacy\n\nDominion splits knowledge across different entities:\n\n| Entity | Sees | Does NOT see |\n|--------|------|-------------|\n| Content relay | Author pubkey, ciphertext, `vault` tag | Recipients (no `p` tags on content) |\n| Gift-wrap relay | Outer gift-wrap metadata | Inner content, CK, tier info |\n| Recipient | CK, decrypted content | Other recipients' CKs |\n\nContent events contain no recipient information. Recipients are managed entirely through the separate gift-wrap channel.\n\n## Expiration\n\nImplementations MAY use NIP-40 `expiration` tags on outer gift-wrap events to facilitate relay cleanup of expired epoch shares. The inner kind 30480 event SHOULD NOT carry an expiration tag, as it is never seen by relays directly.\n\n## Relationship to Existing NIPs\n\n| NIP | How This NIP Uses It |\n|-----|---------------------|\n| NIP-01 | Vault-encrypted events are standard Nostr events on any NIP-01 relay |\n| NIP-44 | CK shares are NIP-44 encrypted to recipients; vault config is NIP-44 self-encrypted |\n| NIP-59 | CK distribution uses gift-wrapped events for metadata privacy |\n| NIP-78 | Vault config is stored as NIP-78 app-specific data (kind 30078) |\n\n### Why Not NIP-44 Direct Encryption?\n\nNIP-44 encrypts content to a single recipient. Encrypting to N recipients requires N separate NIP-44 operations per event. This NIP encrypts content once with an epoch CK and distributes the lightweight key separately.\n\n### Why Not NIP-EE / Marmot (MLS)?\n\nNIP-EE (superseded by the Marmot Protocol) uses MLS ratchet trees for secure group messaging with forward secrecy and post-compromise security. It is designed for chat — all group members are equal, there are no audience tiers, and clients must maintain ratchet tree state. This NIP is designed for content access control — audience tiers, stateless CK derivation, and one-to-many content encryption. The two are complementary.\n\n### Why Not NIP-29 Relay-Based Groups?\n\nNIP-29 delegates access control to relay policy enforcement — the relay decides who can read. This NIP uses cryptographic enforcement — only CK holders can decrypt, regardless of which relay stores the event. NIP-29 also requires custom relay software. The two are complementary.\n\n### Why Not NIP-51 Lists for Tier Membership?\n\nNIP-51 lists are visible to relay operators. Tier memberships (who is in your \"family\" or \"close friends\") are private — stored as NIP-44 self-encrypted data in the vault config.\n\n### Why Not PR #2156 (Exclusive/Paywall Content)?\n\nPR #2156 defines relay-enforced whitelist access with membership events and tier tags. It relies entirely on relay trust (NIP-42 AUTH) — the relay decides who can read. This NIP uses cryptographic enforcement — only CK holders can decrypt, regardless of relay behaviour. The two approaches are complementary: relay-enforced gating provides a fast first layer, cryptographic gating provides guarantees.\n\n### Why Not PR #2258 (Dual Encryption / ECDH Delegated Access)?\n\nPR #2258 defines publisher-level and recipient-level encryption tiers using ECDH-derived keys, targeting healthcare and enterprise use cases. It distributes pre-computed shared secrets via encrypted grant events. However, it has no epoch-based key rotation (revocation requires full keypair rotation), no HKDF content key derivation (keys are ECDH-derived), and no Shamir recovery. This NIP's epoch model provides time-bounded forward secrecy without keypair rotation.\n\n### Why Not PR #2207 (Friends-Only Notes)?\n\nPR #2207 encrypts notes with a symmetric ViewKey distributed via gift wrap, with named circles for audience segmentation. The critical difference is that PR #2207's ViewKey is static: if a single ViewKey is compromised, all past and future content encrypted with that ViewKey is exposed. Dominion's epoch CKs are time-bounded -- a compromised CK exposes only that epoch's content. Beyond that, PR #2207 lacks HKDF derivation (keys are random, not deterministic), per-tier key isolation, and revocation semantics. This NIP adds time-bounded keys and deterministic derivation on top of a similar distribution model.\n\n### Why Not nip4e (PR #1647)?\n\nnip4e proposes decoupling encryption keys from identity keys. Dominion intentionally derives CKs from the author's private key via HKDF so that derivation is stateless -- no separate key management infrastructure is needed. If nip4e merges, Dominion could derive CKs from a nip4e encryption key instead of the Nostr identity key, with no structural changes to the epoch or distribution model.\n\n### Why Not NIP-112 (PR #580)?\n\nNIP-112 (Encrypted Group Events) uses shared-secret group encryption with key rotation, targeting group chat where all members are equal participants. Dominion targets a different problem shape: content access control with audience tiers, one-to-many broadcast, and per-tier key isolation. Group chat requires bidirectional communication between equal peers; content access control requires unidirectional broadcast from an author to tiered audiences.\n\n## New Event Kinds and Tags\n\n| Element | Type | Description |\n|---------|------|-------------|\n| Kind 30480 | Parameterised replaceable | Vault share — epoch CK for a specific recipient |\n| `[\"vault\", \"<epoch_id>\", \"<tier>\"]` | Content event tag | Signals Dominion encryption on any event kind |\n| `[\"algo\", \"<algorithm>\"]` | Protocol event tag | Asymmetric algorithm identifier (default: `secp256k1`) |\n| `[\"L\", \"dominion\"]` / `[\"l\", \"...\", \"dominion\"]` | Label tags (NIP-32) | Protocol namespace |\n\n## Validation Rules\n\n| ID | Rule |\n|----|------|\n| V-DM-01 | The `vault` tag on encrypted events MUST have exactly 3 elements: tag name, epoch_id, tier. |\n| V-DM-02 | Epoch IDs MUST match one of three ISO 8601 formats: daily `YYYY-MM-DD`, weekly `YYYY-Www`, or monthly `YYYY-MM`. |\n| V-DM-03 | Kind 30480 MUST include tags: `d` (format `{epoch_id}:{tier}`), `p` (recipient pubkey), `tier`, `algo`. |\n| V-DM-04 | Kind 30480 `content` MUST be a 64-character lowercase hex string (32-byte CK). |\n| V-DM-05 | Kind 30480 MUST be NIP-44 encrypted and NIP-59 gift-wrapped before publishing. Implementations MUST NOT publish kind 30480 events in plaintext. |\n| V-DM-06 | Vault config (kind 30078 with `d` = `dominion:vault-config`) `content` MUST be NIP-44 self-encrypted. |\n| V-DM-07 | Encrypted content MUST be `base64(iv \\|\\| ciphertext \\|\\| tag)` where IV is 12 bytes and tag is 16 bytes. |\n| V-DM-08 | CK derivation MUST use HKDF-SHA256 with salt `dominion-ck-v1` and info `epoch:{epoch_id}:tier:{tier_name}`. |\n| V-DM-09 | Revoked pubkeys in vault config MUST be skipped during epoch CK distribution. Implementations MUST NOT distribute CKs to revoked pubkeys. |\n| V-DM-10 | The `d` tag on kind 30480 MUST match the format `{epoch_id}:{tier}`. Both components are REQUIRED. |\n\n## Security Considerations\n\n### No forward secrecy\n\nCKs are derived from the author's private key via HKDF. If the private key is compromised, all past and future CKs for all epochs and tiers are derivable. This is a conscious trade-off for stateless derivation. High-security applications SHOULD use short epoch lengths (daily) and consider MLS-based alternatives (NIP-EE/Marmot) for forward secrecy.\n\n### Epoch granularity vs revocation speed\n\nWeekly epochs mean a revoked recipient can decrypt up to 7 days of content after revocation. Daily epochs reduce this window but increase distribution overhead. Applications SHOULD choose epoch length based on their threat model.\n\n### CK caching by recipients\n\nRecipients cache CKs locally. Once a CK is distributed, it cannot be revoked -- only future epoch CKs can be withheld. Content already decrypted cannot be \"un-shown.\"\n\n### Vault config as high-value target\n\nThe NIP-78 vault config contains the complete social graph (tier memberships, individual grants, revoked pubkeys). Compromise of this event reveals who is in which audience tier. It is self-encrypted but stored on relays.\n\n### Gift-wrap relay metadata\n\nWhile content relays see no recipient information, gift-wrap relays see outer envelope metadata (sender timestamp, recipient pubkey). Use multiple relays and consider timing decorrelation.\n\n### NIP-46 signer implications\n\nCK derivation requires access to the author's private key. NIP-46 remote signing implementations must handle HKDF derivation at the signer, not the client. The private key MUST NOT be sent to the client for local derivation.\n\n### Key zeroisation\n\nImplementations MUST zeroise CK byte buffers after use. JavaScript string values are immutable and cannot be erased; implementations SHOULD minimise CK string lifetimes.\n\n## Test Vectors\n\n### HKDF-SHA256 Derivation\n\n| Parameter | Value |\n|-----------|-------|\n| IKM (private key) | `0101010101010101010101010101010101010101010101010101010101010101` (32 bytes of `0x01`) |\n| Salt | `dominion-ck-v1` |\n| Info | `epoch:2026-W10:tier:family` |\n| Output length | 32 bytes |\n| Expected CK | `<computed-hex-ck>` |\n\nImplementations SHOULD verify their HKDF library output against this vector. The expected CK value depends on the HKDF-SHA256 implementation; compute it with a known-good library and use as a regression test.\n\n### AES-256-GCM Round-Trip\n\n| Parameter | Value |\n|-----------|-------|\n| CK | `0101010101010101010101010101010101010101010101010101010101010101` |\n| Plaintext | `Hello, Dominion!` |\n| IV | `000102030405060708090a0b` (12 bytes) |\n\nEncrypt the plaintext with the CK and IV using AES-256-GCM. The output is `base64(iv \\|\\| ciphertext \\|\\| tag)`. Decrypt with the same CK and verify the plaintext round-trips.\n\n### Invalid Event Examples\n\nThe following events MUST be rejected by conforming implementations:\n\n**Missing `vault` tag on encrypted content:**\n\n```json\n{\n  \"kind\": 1,\n  \"content\": \"<base64-encrypted-content>\",\n  \"tags\": []\n}\n```\n\nEncrypted content with no `vault` tag is indistinguishable from plaintext. Implementations MUST NOT attempt Dominion decryption on events without a `vault` tag.\n\n**Kind 30480 with non-hex CK content:**\n\n```json\n{\n  \"kind\": 30480,\n  \"content\": \"not-a-hex-string\",\n  \"tags\": [[\"d\", \"2026-W10:family\"], [\"p\", \"<pubkey>\"], [\"tier\", \"family\"], [\"algo\", \"secp256k1\"]]\n}\n```\n\nThe `content` field MUST be a 64-character lowercase hex string. Implementations MUST reject non-hex or wrong-length values.\n\n**Kind 30480 published without gift wrap:**\n\n```json\n{\n  \"kind\": 30480,\n  \"content\": \"<hex-ck>\",\n  \"tags\": [[\"d\", \"2026-W10:family\"], [\"p\", \"<pubkey>\"], [\"tier\", \"family\"], [\"algo\", \"secp256k1\"]]\n}\n```\n\nKind 30480 events MUST NOT be published in plaintext. Implementations MUST reject any kind 30480 event received outside a NIP-59 gift wrap.\n\n## Dependencies\n\n| NIP | Usage |\n|-----|-------|\n| NIP-01 | Basic protocol flow, addressable events |\n| NIP-32 | Labelling (protocol namespace tags) |\n| NIP-40 | Expiration timestamps (gift-wrap cleanup) |\n| NIP-44 | Versioned encrypted payloads (CK encryption, vault config self-encryption) |\n| NIP-59 | Gift wrap (CK distribution privacy) |\n| NIP-78 | App-specific data (vault config storage) |\n\n## Interoperability\n\nAny Nostr client can add decryption support with approximately 100 lines of code:\n\n1. Gift-wrap unwrapping (NIP-59) — ~50 lines\n2. AES-256-GCM decryption (WebCrypto or equivalent) — ~30 lines\n3. Epoch CK caching — ~20 lines\n\n## Limitations\n\n| Limitation | Mitigation |\n|-----------|-----------|\n| Forward-only revocation | Weekly epochs cap exposure at 7 days. Same model as Signal/WhatsApp. |\n| CK cached by recipient | Inherent to all E2E systems — cannot un-show a message. |\n| Epoch granularity (not per-event) | Use NIP-44 directly for per-event access control. |\n| Author must be online to distribute | CKs are distributed by the author's client. Offline authors delay new recipient access. |\n| Key derivation requires private key | NIP-46 remote signing implementations must handle CK derivation at the signer. |\n\n## Reference Implementation\n\n[`dominion-protocol`](https://github.com/forgesworn/dominion) — TypeScript, MIT licence. Two-layer exports:\n\n- `dominion-protocol` — HKDF key derivation, AES-256-GCM encryption, Shamir secret sharing, config management\n- `dominion-protocol/nostr` — kind 30480 and NIP-78 event builders/parsers\n","sig":"9ba83caec940f82102eb1e323b0e0a0c77de024e85ce02175135191774c8ada1dd8c3db4197ba398e9f34a0496c8c540e2cd26aefd57c59d92a0ee9fc14371f0"}