Souspli

Souspli / Docs / Design notes

Design note — 2026-07-23. A decision record, written when this was built and kept as written. It uses the code's names (thing, shell, cage — see the glossary) and may describe things that have since changed. Current documentation: locators.

Build brief — naming

Phase 5, and the last piece before conformance. Transport answered how bytes arrive; naming answers whose they are and where to find them by a human name. It turns alice.eth into two things: the author key that a thing must be signed by to be "hers" (identity), and a locator to fetch her latest thing (discovery).

Depends on: the merged shell (admission, library, mount, transport) and the existing interface Resolver stub in src/shell/naming/.

The principle, restated for naming: the resolver is untrusted for the KEY; the signature is. A name is only ever shown as verified when the resolver's answer provably matches the admitted thing's (signature-proven) author key. A malicious or wrong resolver can fail to resolve, or point at a different key — but it can never make the shell attach a name to a thing whose author key does not match the resolver's own answer. The key comes from the signature (admission already proved it); naming only adds the human name on top, and only when it checks out.


1. Two jobs, one service

NamingService does exactly two things, both dispatched to registered resolvers by name/scheme:

// Discovery: a human name → a transport locator to fetch.
resolve(name: string): Promise<string /* locator */>

// Trust: the primary verified name for an author key (reverse + forward
// confirmed), for the per-thing header. Or `unresolvable`.
primaryName(authorScheme: string, authorKeyHex: string): Promise<NameVerification>

// Trust: does this name map to this author key? (forward direction, used when
// you fetched a thing BY a name and want to confirm you got the right author.)
verifyName(name: string, authorScheme: string, authorKeyHex: string): Promise<NameVerification>
type NameVerification =
  | { status: 'verified'; name: string }          // resolver's answer == the thing's key
  | { status: 'mismatch'; name: string; resolvedKey: string } // resolves to a DIFFERENT key — an alarm
  | { status: 'unresolvable' }                    // no name / cannot resolve

verified is the ONLY status that earns a name in trusted chrome. mismatch is styled as a warning (the discovery layer handed you someone else's content under a name), distinct from unresolvable (no name known — show the raw key).

2. ENS (primary, via viem)

ENS matches eth-eip191: an ENS name resolves to a 20-byte address, which is exactly author.k for an eth-signed thing.

The ENS reads go through an injected EnsClient interface (getAddress / getName / getText). Production supplies a viem-backed client against an RPC (ens-viem.ts); it is exercised manually, not in CI. Every test uses an in-memory mock client — so the whole trust property is deterministic without a network, and viem's live path is the only untested part.

viem is loaded lazily (like webtorrent): the live ENS path degrades with a clear "install viem" error rather than crashing if it is absent; the mock path never touches it.

3. Direct locators pass through

thing: / bundle: / magnet: / file: are already locators — the DirectResolver returns them unchanged from resolve, and has no name (so primaryName for a raw-hash author is unresolvable). Direct-hash ingestion keeps working with no resolver in the room — the most honest test that the core holds without any chain.

4. Nostr — stubbed behind the interface

npub… / NIP-05 discovery + relay subscriptions are wired as a NostrResolver that reports "not yet supported". Its slot proves the abstraction holds a second naming system; the implementation is a later pass (relays = network + state).

5. Shell wiring

6. Tests (deterministic, mock ENS — no network)

7. Definition of done

A NamingService with resolve (discovery) and primaryName / verifyName (trust); ENS via an injected EnsClient with reverse+forward-confirmed name verification and text-record discovery, viem-backed for live use and mock-tested; direct locators passing through; Nostr stubbed; the shell resolving names to locators for fetch and showing a verified name in the trust header only when it provably matches the thing's author key; a deterministic test battery proving a wrong/malicious resolver can never attach a name to the wrong author.

8. Out of scope (// LATER:)

Nostr relay discovery + subscriptions; ENS key rotation / revocation (format gap #3); watching a chain/relay for new things (the feed is still ingest-driven); DNSSEC / other name systems; caching/TTL policy for resolutions.