Phase 2.5 / follow-up of #437. Auto-publish a NIP-05 mapping for any pod that ran with --provision-keys, so the pod owner's Nostr-flavoured identity becomes discoverable as name@domain by other Nostr clients.
Why
Phase 2 wired the provisioned key into the WebID profile and added the did:nostr: controller. NIP-05 is the standard "human-friendly identifier" layer on top of a Nostr pubkey: clients fetch https://<domain>/.well-known/nostr.json?name=<name> and expect { "names": { "<name>": "<pubkey hex>" } } (optionally with relays). JSS now has the pubkey and the domain; serving NIP-05 is one small route handler.
Shape
A single route handler at /.well-known/nostr.json. Returns a { names, relays?, nip46? } document built from whichever pods on this server have an owner key.
Per-pod vs shared aggregated
- Subdomain mode (--subdomains + --base-domain): pods live at alice.example.com, bob.example.com. Each subdomain naturally serves its own .well-known/nostr.json — "one per pod" falls out for free. The handler reads the requesting Host, finds the matching pod, returns just that pod's mapping.
- Path mode (default multi-user): pods live at example.com/alice/, example.com/bob/. There's only one origin, so only one .well-known/nostr.json. The handler aggregates: { names: { alice: "<pubkey-a>", bob: "<pubkey-b>", … } } for every pod with a provisioned key.
- Single-user: trivially one mapping. Default name from --single-user-name (me by default), single entry.
The handler doesn't need separate code paths per mode — read the Host, list local pods, filter by host visibility, emit the mapping.
Design questions to settle
- Default name. For a pod at /alice/, default the NIP-05 name to alice (matches the pod name)? Or require an explicit --nip05-name (or nip05Name in pod-creation body) to opt in per-pod?
- Auto-include relays when --nostr is on? The pod's own /relay is an obvious entry. Could be [ "wss://<host>/relay" ]. Operators with their own relay infrastructure might want to override.
- Auto-include nip46 (remote signer) when applicable, or leave for a follow-up?
- Discovery boundary. NIP-05 names are public — surfacing every pod's name on /.well-known/nostr.json reveals the operator's pod inventory. Should multi-user mode default to opt-in per pod (operator-controlled) rather than auto-listing every pod?
- Source of truth. Read the pubkey from <pod>/private/privkey.jsonld (privileged) or from <pod>/profile/card.jsonld verificationMethod (public)? The latter is simpler — the public side is already exposed there since feat: --provision-keys Phase 2 — wire the owner key into the WebID profile, close the LWS-CID loop (#437) #443.
My defaults if no answer: default name from pod name, opt-out via config (not opt-in — most operators want the discovery), auto-include wss://<host>/relay when --nostr is on, skip nip46 for now, source from the WebID profile's VM (no need to read the secret file).
Acceptance
- GET /.well-known/nostr.json?name=<name> returns { names: { <name>: <pubkey-hex> } } for any pod with a provisioned key visible to the requesting host.
- Subdomain mode: per-host filtering — alice.example.com/.well-known/nostr.json returns only alice's mapping.
- Path mode: aggregated — example.com/.well-known/nostr.json returns all pods.
- relays field auto-populated with the local relay URL when --nostr is on.
- ?name= query: when present, return only that name (per NIP-05); when absent, return everything.
- Public read (no WAC) — NIP-05 verifiers don't authenticate.
- Tests: route returns expected JSON for single-user, multi-user path mode, multi-user subdomain mode; query-filter; relay inclusion when --nostr on.
Out of scope
- NIP-46 remote signer (could land in a follow-up if there's demand).
- Per-pod overrides for relays / extra metadata. Phase 2.5 ships defaults; later iterations can add knobs.
- Cross-domain proxying / CORS for NIP-05 verification beyond the standard Access-Control-Allow-Origin: *.
Refs #437.
Phased plan
- MVP (sub-issue feat: NIP-05 MVP — /.well-known/nostr.json for single-user pods (#445) #446) — single-user only, single name, no extras. Just GET /.well-known/nostr.json → { names: { <single-user-name>: <hex> } } from the profile VM. No relays, no ?name= filter, no aggregation. Multi-user returns 404.
- Phase 2 — multi-user aggregation: path mode emits one combined index, subdomain mode filters per host. Adds the ?name= query filter per NIP-05 §3.
- Phase 3 — relays field auto-populated when --nostr is on; per-pod opt-out / custom name knobs; nip46 if there's demand.
Phase 2.5 / follow-up of #437. Auto-publish a NIP-05 mapping for any pod that ran with --provision-keys, so the pod owner's Nostr-flavoured identity becomes discoverable as name@domain by other Nostr clients.
Why
Phase 2 wired the provisioned key into the WebID profile and added the did:nostr: controller. NIP-05 is the standard "human-friendly identifier" layer on top of a Nostr pubkey: clients fetch https://<domain>/.well-known/nostr.json?name=<name> and expect { "names": { "<name>": "<pubkey hex>" } } (optionally with relays). JSS now has the pubkey and the domain; serving NIP-05 is one small route handler.
Shape
A single route handler at /.well-known/nostr.json. Returns a { names, relays?, nip46? } document built from whichever pods on this server have an owner key.
Per-pod vs shared aggregated
The handler doesn't need separate code paths per mode — read the Host, list local pods, filter by host visibility, emit the mapping.
Design questions to settle
My defaults if no answer: default name from pod name, opt-out via config (not opt-in — most operators want the discovery), auto-include wss://<host>/relay when --nostr is on, skip nip46 for now, source from the WebID profile's VM (no need to read the secret file).
Acceptance
Out of scope
Refs #437.
Phased plan