lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

profile-metadata.md (9487B)


      1 # Profile metadata contract
      2 
      3 Status: canonical
      4 
      5 This contract defines the public kind-`0` Profile metadata boundary used by
      6 Radroots strict authoring and tolerant inbound projection. It is based on the
      7 pinned NIP-01, NIP-05, and NIP-24 documents at NIPs commit
      8 `bdfa7e62ef87fcfcb992b1a27aee49d36b0b4f91` and the Blossom protocol at commit
      9 `b5bd2801d1763aa635fc8fea7a76597e0eb18990`.
     10 
     11 ## Operation authority
     12 
     13 | Operation | Boundary | Signing | Transport |
     14 | --- | --- | --- | --- |
     15 | `event.verify_nip01` | exact identifier and Schnorr verification to a non-forgeable wrapper | NIP-01 | none |
     16 | `event.select_head` | NIP-01 replaceable/addressable timestamp and lowest-id selection | none | none |
     17 | `profile.build_authored_draft` | strict authored metadata to kind-`0` wire parts | none | none |
     18 | `profile.parse_inbound_metadata` | JSON object to tolerant inbound metadata | none | none |
     19 | `profile.verify_and_admit_event` | verified exact kind-`0` envelope to tolerant metadata bound to that envelope | NIP-01 | none |
     20 
     21 `profile.parse_inbound_metadata` is a content parser, not an event-acceptance
     22 boundary. A caller must supply content from a kind-`0` event only after the
     23 event identifier and signature have been verified. The authoritative combined
     24 boundary is `profile.verify_and_admit_event`: it recomputes the identifier,
     25 verifies the Schnorr signature, requires exact kind `0`, and only then invokes
     26 the same tolerant parser. It accepts standard tagless Profile events and does
     27 not require a Radroots marker tag.
     28 
     29 `event.verify_nip01` and its `RadrootsSignatureVerifiedEvent` result are
     30 available through the codec's `nostr` feature without enabling `knowledge`.
     31 Envelope conversion requires the author to be a valid secp256k1 x-only public
     32 key; a canonical-length hex value that is not a curve point returns
     33 `malformed_envelope`, not `signature_invalid`. Knowledge contract validation and
     34 decoding are a later optional stage and cannot be substituted for general event
     35 verification.
     36 
     37 The direct legacy `Profile` codec, Profile-specific Nostr/network
     38 publish helpers, and replica Profile draft emission were removed in the
     39 `1.0.0-alpha.1` breaking contract. A replica Profile row is a lossy inbound
     40 projection and cannot prove author intent for a complete kind-`0` replacement.
     41 New authored callers must use `profile.build_authored_draft`. The opaque
     42 Radroots generic builder rejects kind `0` at direct-signing and client
     43 publication boundaries, so it cannot substitute for Profile operation
     44 authority.
     45 
     46 `RadrootsNostrClient` no longer dereferences implicitly to the upstream SDK
     47 client, so upstream Profile conveniences are not exposed through ordinary
     48 method resolution. `from_inner` and `into_inner`, externally supplied unsigned
     49 events and NIP-46 signing, and already-signed event relay remain explicit
     50 low-level interoperability boundaries. Their results carry no Radroots typed
     51 product-authoring claim; once a caller takes one of those boundaries, upstream
     52 protocol behavior is outside the Radroots authored-operation contract.
     53 
     54 Legacy `profile::decode`, Nostr Profile adapters/fetchers, the network Profile
     55 fetch methods, and replica Profile ingest remain read-side compatibility paths.
     56 They may require or coerce legacy fields, discard unprojected metadata, or rely
     57 on legacy marker tags. They are not `profile.parse_inbound_metadata`, do not
     58 establish verified event admission, and must not be used as its substitute.
     59 
     60 ## Strict authored boundary
     61 
     62 `AuthoredProfile` has private fields and requires a non-whitespace,
     63 control-free `name`, consistent with the NIP-24 recommendation that `name`
     64 remain present when `display_name` is used. The scoped optional fields are
     65 `display_name`, `about`, `nip05`, `bot`, `picture`, and `banner`; `bot` is a
     66 Boolean. NIP-05 values enter only through `Nip05Identifier`. Picture and
     67 banner values enter through the shared `AuthoredImage`, which can wrap
     68 only an `image/*` `ByteVerifiedDescriptor`. There is no raw-string
     69 media setter or unchecked deserialization path. Generic `website`, `lud06`, and
     70 `lud16` strings remain outside this strict authored operation.
     71 
     72 Kind `0` is whole-object replaceable. Strict authored output is therefore a
     73 complete replacement snapshot, never a patch: every omitted existing standard,
     74 residual, or custom field is removed by the replacement. This operation does
     75 not merge the tolerant inbound raw object. It is safe for initial Profile
     76 creation or an explicitly confirmed full replacement; it must not power a
     77 silent partial edit. Retaining an existing picture or banner requires the
     78 runtime to re-fetch or retain the bytes, re-establish the byte-verified image
     79 descriptor, and satisfy BUD-02 before signing. Until such a full-snapshot edit
     80 pipeline exists, clients must keep existing Profile editing read-only.
     81 
     82 The authored codec emits:
     83 
     84 - kind `0`
     85 - no tags
     86 - one JSON object with fields in this deterministic order when present:
     87   `name`, `display_name`, `about`, `picture`, `banner`, `nip05`, `bot`
     88 - only the descriptor URL for each media field
     89 - at most 131072 UTF-8 bytes of metadata content
     90 
     91 The descriptor state proves that approved descriptor hash, size, and media type
     92 match supplied bytes. It does not prove that BUD-02 upload completed or that a
     93 blob is network-retrievable, and it does not inspect or sanitize the image
     94 format. A publication runtime must require successful BUD-02 completion before
     95 signing media-bearing output. A consuming media runtime remains responsible for
     96 decode and format-safety policy.
     97 
     98 For two verified kind-`0` events from the same author, the head is the event
     99 with the greater `created_at`. Equal timestamps select the lexicographically
    100 lowest canonical event id, independent of relay or ingestion arrival order.
    101 
    102 ## Tolerant inbound boundary
    103 
    104 `profile.parse_inbound_metadata` accepts a JSON object of at most 131072 UTF-8
    105 bytes without requiring `name`. Correctly typed `name`, `display_name`, `about`,
    106 `picture`, `banner`, `nip05`, and `bot` fields are projected without JSON type
    107 coercion; accepted NIP-05 domains are canonicalized as described below. In
    108 particular, `bot` must be a JSON Boolean. Every string picture or banner is
    109 returned as `RadrootsUnverifiedProfileMediaReference`, including strings that
    110 look like valid Blossom hash-path URLs.
    111 
    112 The result retains:
    113 
    114 - the exact input content
    115 - the complete parsed object
    116 - a residual map containing every unknown field, wrong-typed known field, and
    117   syntactically invalid NIP-05 string
    118 
    119 Oversized content is rejected before parsing. Malformed JSON and non-object
    120 roots are parse errors. Duplicate top-level metadata keys are rejected because
    121 they have ambiguous cross-parser semantics. Optional metadata that this
    122 contract does not project, including NIP-24 `website` and birthday data plus
    123 `lud06` and `lud16`, remains in the residual and complete raw views. Exact
    124 nested JSON text, including any duplicate nested names, remains available
    125 through the raw content view.
    126 
    127 ## NIP-05 boundary
    128 
    129 `Nip05Identifier` requires exactly one `@`, a non-empty local part using
    130 only lowercase `a-z`, digits, `-`, `_`, or `.`, and an ASCII DNS domain.
    131 Domain matching is case-insensitive, so accepted domains are canonicalized to
    132 lowercase. Parsing is syntax-only. It performs no HTTPS lookup and never
    133 represents verified ownership or identity trust.
    134 
    135 A syntax-checked identifier is not a safe network-fetch target by itself. A
    136 future resolver must separately govern HTTPS, redirects, address resolution,
    137 loopback/private/link-local targets, response size, and timeouts.
    138 
    139 ## Stable error codes
    140 
    141 The public error enums are non-exhaustive so future codes can be added without
    142 making downstream matches exhaustive. Current stable codes are:
    143 
    144 | Boundary | Codes |
    145 | --- | --- |
    146 | NIP-05 syntax | `missing_separator`, `multiple_separators`, `invalid_local_part`, `invalid_domain` |
    147 | strict Profile construction | `invalid_name` |
    148 | shared authored image construction | `media_type_not_image` |
    149 | strict Profile encoding | `content_too_large` |
    150 | tolerant inbound parsing | `content_too_large`, `invalid_json`, `root_not_object`, `duplicate_field` |
    151 | NIP-01 event verification | `malformed_envelope`, `kind_out_of_range`, `id_mismatch`, `signature_invalid`, `signature_verification_unavailable` |
    152 | verified Profile admission | NIP-01 codes plus `invalid_kind`, `content_too_large`, `invalid_json`, `root_not_object`, `duplicate_field` |
    153 
    154 Inbound size validation occurs before JSON parsing. For content within the
    155 limit, malformed JSON returns `invalid_json`; a well-formed non-object returns
    156 `root_not_object`; and a well-formed object with a duplicate top-level field
    157 returns `duplicate_field`. Wrong-typed projected fields are residual data, not
    158 parse errors.
    159 
    160 ## Conformance
    161 
    162 The canonical suite is
    163 `contracts/conformance/vectors/profile/metadata.v1.json`. Verified event
    164 admission and replacement use
    165 `contracts/conformance/vectors/profile/verified_event.v1.json`. Both are
    166 mirrored under `crates/event_codec/tests/fixtures/` for published-package
    167 tests. The dispatchers execute every vector against the public APIs and require
    168 canonical and packaged copies to be byte-for-byte equal when the workspace
    169 contract is present. The verified-event suite includes a canonical-id raw event
    170 whose author is not a valid secp256k1 curve point, proving the stable
    171 `malformed_envelope` mapping. Consumers enable `serde_json` for metadata
    172 operations and both `serde_json` and `nostr` for cryptographic Profile
    173 admission.