NAP-OUTBOX / PR #32
Outbox-Aware Relay Access
Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.
NAP-OUTBOX
Outbox-Aware Relay Access
draft
NAP ID: NAP-OUTBOX
Domain: outbox
Depends:
relay— wire · required — importsRelayEventResultfor raw Nostr event returns.resource— wire · optional — importedRelayEventResult.sidecar.resources?carriesResourceSidecarEntry[], a type owned by theresourcedomain. Web binding (NIP-5D):window.napplet.outbox; domain presence signals availability.
Description
NAP-OUTBOX provides napplets with outbox-model relay routing through the shell. A napplet can already query relays directly through NAP-RELAY, but then every napplet must discover NIP-65 relay lists, choose author write relays, fall back when lists are missing, deduplicate events, and keep live subscriptions aligned as relay intelligence changes. NAP-OUTBOX lets a napplet provide Nostr filters and intent; the runtime finds the correct relays, queries them, deduplicates results, and streams updates.
This interface is for Nostr event access where relay selection is part of the result correctness. The shell owns relay discovery, routing, fallback, deduplication, and publish fanout policy.
API Surface
| Operation | Parameters | Result | Wire |
|---|---|---|---|
getEvent |
eventId (tstr), optional options (OutboxEventOptions) |
OutboxEventResult |
outbox.getEvent / outbox.getEvent.result |
query |
filters (NostrFilter or list), optional options (OutboxQueryOptions) |
OutboxResult |
outbox.query / outbox.query.result |
subscribe |
filters (NostrFilter or list), optional options (OutboxSubscribeOptions) |
OutboxSubscription handle |
outbox.subscribe plus push messages |
publish |
template (EventTemplate), optional options (OutboxPublishOptions) |
OutboxPublishResult |
outbox.publish / outbox.publish.result |
resolveRelays |
target (OutboxTarget) |
OutboxRelayPlan |
outbox.resolveRelays / outbox.resolveRelays.result |
Schemas
RelayEventResult is owned by NAP-RELAY. NAP-OUTBOX imports it by name for
every raw Nostr event it returns. Its sidecar carries optional
resources and relayHints; relay hints replace NAP-OUTBOX's former top-level
relays result map for returned events.
Primitive references:
| Name | Meaning |
|---|---|
NostrFilter |
NIP-01 filter object. |
NostrEvent |
NIP-01 signed event object. |
EventTemplate |
Unsigned event template for shell signing. |
RelayEventResult |
External type owned by NAP-RELAY. |
Enumerations:
| Name | Values |
|---|---|
OutboxTarget.direction |
read, write |
OutboxRelayPlan.source |
nip65, cache, policy, fallback |
OutboxEventOptions:
| Field | Type | Required | Description |
|---|---|---|---|
author |
tstr |
no | Author pubkey hint for relay discovery. |
relays |
list of tstr |
no | Relay URL hints or policy override candidates. |
timeoutMs |
uint |
no | Request timeout in milliseconds. |
OutboxQueryOptions:
| Field | Type | Required | Description |
|---|---|---|---|
authors |
list of tstr |
no | Author pubkey hints for relay discovery. |
relays |
list of tstr |
no | Relay URL hints or policy override candidates. |
limit |
uint |
no | Maximum number of events to return. |
timeoutMs |
uint |
no | Request timeout in milliseconds. |
OutboxSubscribeOptions:
| Field | Type | Required | Description |
|---|---|---|---|
authors |
list of tstr |
no | Author pubkey hints for relay discovery. |
relays |
list of tstr |
no | Relay URL hints or policy override candidates. |
limit |
uint |
no | Maximum stored events to backfill. |
timeoutMs |
uint |
no | Initial request timeout in milliseconds. |
OutboxPublishOptions:
| Field | Type | Required | Description |
|---|---|---|---|
relays |
list of tstr |
no | Explicit relay URL fanout candidates, subject to shell validation. |
toOutbox |
bool |
no | Defaults to true. Publish to the shell-user's NIP-65 write relays. |
toInboxes |
list of tstr |
no | Author pubkeys whose NIP-65 read relays are required publish fanout targets. |
OutboxTarget:
| Field | Type | Required | Description |
|---|---|---|---|
authors |
list of tstr |
no | Author pubkeys for relay discovery. |
pubkey |
tstr |
no | Single pubkey target. |
direction |
read or write |
no | Relay direction to resolve. |
OutboxRelayPlan:
| Field | Type | Required | Description |
|---|---|---|---|
relays |
list of tstr |
yes | Candidate relay URLs. |
source |
nip65, cache, policy, or fallback |
yes | Source of the relay plan. |
missingAuthors |
list of tstr |
no | Authors whose relay lists could not be resolved. |
OutboxEventResult:
| Field | Type | Required | Description |
|---|---|---|---|
result |
RelayEventResult |
no | Matching event result when found. |
incomplete |
bool |
no | True when lookup results are partial. |
error |
tstr |
no | Error reason when lookup failed. |
OutboxResult:
| Field | Type | Required | Description |
|---|---|---|---|
events |
list of RelayEventResult |
yes | Deduplicated event results. |
incomplete |
bool |
no | True when results are partial. |
error |
tstr |
no | Error reason when the query failed. |
OutboxSubscription:
| Member | Type | Required | Description |
|---|---|---|---|
on("event", cb) |
function | yes | Registers a callback for outbox.event deliveries. The callback receives RelayEventResult. |
on("closed", cb) |
function | yes | Registers a callback for outbox.closed. The callback receives optional reason. |
close() |
function | yes | Closes the subscription by sending outbox.close for the handle's subscription ID. |
OutboxPublishResult:
| Field | Type | Required | Description |
|---|---|---|---|
ok |
bool |
yes | True when publish succeeded. |
event |
NostrEvent |
no | Signed event produced by the shell. |
eventId |
tstr |
no | Event ID of the signed event. |
relays |
map of relay URL tstr to bool |
no | Per-relay publish result. |
error |
tstr |
no | Error reason when publish failed. |
getEvent(eventId, options?) -- Fetches one event by ID through shell-owned relay routing. If options.author is present, the shell SHOULD use that author's outbox relays as the primary read target. If no author is known, the shell MAY use relay hints, cache, policy relays, fallback relays, or relay intelligence. The shell validates the event ID and signature before returning it as RelayEventResult.
query(filters, options?) -- Performs a one-shot outbox-aware query. The shell derives authors from filters.authors, options.authors, tag hints, or shell policy, resolves the relevant relays, queries them, deduplicates events by id, merges relay hints into each RelayEventResult.sidecar.relayHints, and returns the collected results.
subscribe(filters, options?) -- Opens a live outbox-aware event stream. Outbox routing may span multiple relays and may change over time, so the napplet-facing lifecycle is the subscription handle plus outbox.close and outbox.closed.
publish(template, options?) -- Signs the template once and publishes that signed event through outbox-aware relay fanout. Unless options.toOutbox is false, the shell includes the shell-user's NIP-65 write relays. When options.toInboxes is non-empty, the shell MUST resolve those authors' NIP-65 read relays and include each resolved, policy-allowed relay in the fanout set. The shell also includes validated options.relays entries. The final publish target set is the deduplicated union of requested own-outbox relays, requested inbox relays, and validated explicit relay URLs. If the shell cannot satisfy a required publish fanout target, it MUST report the failure through OutboxPublishResult.
resolveRelays(target) -- Returns the relay plan the shell would use for a read or write target. This is useful for diagnostics and UI, but napplets SHOULD prefer query, subscribe, or publish instead of manually replaying the plan through NAP-RELAY.
Wire Protocol
outbox.* messages use the NIP-5D wire format ({ "type": "domain.action", ...payload }).
| Type | Direction | Payload fields |
|---|---|---|
outbox.getEvent |
napplet -> shell | id, eventId, options? |
outbox.getEvent.result |
shell -> napplet | id, result?, incomplete?, error? |
outbox.query |
napplet -> shell | id, filters, options? |
outbox.query.result |
shell -> napplet | id, events, incomplete?, error? |
outbox.subscribe |
napplet -> shell | id, subId, filters, options? |
outbox.event |
shell -> napplet | subId, result |
outbox.closed |
shell -> napplet | subId, reason? |
outbox.close |
napplet -> shell | id, subId |
outbox.publish |
napplet -> shell | id, event, options? |
outbox.publish.result |
shell -> napplet | id, ok, event?, eventId?, relays?, error? |
outbox.resolveRelays |
napplet -> shell | id, target |
outbox.resolveRelays.result |
shell -> napplet | id, plan, error? |
Key design notes:
filtersare NIP-01 filters, either one filter or an array of filters.RelayEventResult.sidecar.relayHintsrecords relay URLs where the shell observed returned events, when the shell can disclose them.RelayEventResult.sidecar.resourcescarries resource sidecars when the shell pre-resolves bytes under NAP-RESOURCE policy.OutboxPublishOptions.toOutboxdefaults totrue.OutboxPublishOptions.toInboxesis a delivery contract for recipient inbox fanout, not a hint.options.relaysis an explicit relay URL fanout set forpublish, and a hint or policy override candidate for read surfaces. It never bypasses shell ACLs.outbox.getEventis a request;outbox.eventremains the subscription push message.- The shell may internally use NAP-RELAY semantics, but napplets consume this higher-level outbox interface.
Examples
Fetch a single event with an author hint:
-> {
"type": "outbox.getEvent",
"id": "e1",
"eventId": "ev1...",
"options": { "author": "ab12...", "timeoutMs": 3000 }
}
<- {
"type": "outbox.getEvent.result",
"id": "e1",
"result": {
"event": { "id": "ev1...", "pubkey": "ab12...", "kind": 1, "content": "hello", "tags": [], "created_at": 1234567890, "sig": "..." },
"sidecar": { "relayHints": ["wss://relay.example.com"] }
}
}
Query an author's notes from their outbox relays:
-> {
"type": "outbox.query",
"id": "q1",
"filters": [{ "authors": ["ab12..."], "kinds": [1], "limit": 20 }],
"options": { "timeoutMs": 3000 }
}
<- {
"type": "outbox.query.result",
"id": "q1",
"events": [
{
"event": { "id": "ev1...", "pubkey": "ab12...", "kind": 1, "content": "hello", "tags": [], "created_at": 1234567890, "sig": "..." },
"sidecar": { "relayHints": ["wss://relay.example.com"] }
}
]
}
Live subscription:
-> {
"type": "outbox.subscribe",
"id": "s1",
"subId": "sub-1",
"filters": [{ "authors": ["ab12..."], "kinds": [1], "limit": 50 }],
"options": { "timeoutMs": 3000 }
}
<- { "type": "outbox.event", "subId": "sub-1", "result": { "event": { "id": "ev1...", "pubkey": "ab12...", "kind": 1, "content": "hello", "tags": [], "created_at": 1234567890, "sig": "..." }, "sidecar": { "relayHints": ["wss://relay.example.com"] } } }
Publish a reply through the user's outbox and referenced inboxes:
-> {
"type": "outbox.publish",
"id": "p1",
"event": { "kind": 1111, "content": "reply from a napplet", "tags": [["p", "ab12..."]], "created_at": 1234567890 },
"options": { "toInboxes": ["ab12..."] }
}
<- {
"type": "outbox.publish.result",
"id": "p1",
"ok": true,
"event": { "id": "ev2...", "pubkey": "user...", "kind": 1111, "content": "reply from a napplet", "tags": [["p", "ab12..."]], "created_at": 1234567890, "sig": "..." },
"eventId": "ev2...",
"relays": { "wss://relay.example.com": true }
}
Publish to explicit relay targets without the user's public outbox:
-> {
"type": "outbox.publish",
"id": "p2",
"event": { "kind": 1059, "content": "...", "tags": [["p", "ab12..."]], "created_at": 1234567890 },
"options": { "toOutbox": false, "relays": ["wss://dm.example.com"] }
}
<- {
"type": "outbox.publish.result",
"id": "p2",
"ok": true,
"event": { "id": "ev3...", "pubkey": "user...", "kind": 1059, "content": "...", "tags": [["p", "ab12..."]], "created_at": 1234567890, "sig": "..." },
"eventId": "ev3...",
"relays": { "wss://dm.example.com": true }
}
Resolve relay plan:
-> { "type": "outbox.resolveRelays", "id": "r1", "target": { "pubkey": "ab12...", "direction": "read" } }
<- { "type": "outbox.resolveRelays.result", "id": "r1", "plan": { "relays": ["wss://relay.example.com"], "source": "nip65" } }
Error Handling
Result messages MAY include error. Common errors include "not found", "no authors", "relay list unavailable", "relay timeout", "policy denied", "publish denied", and "invalid filter".
If the shell returns partial results because some relay lists or relay connections failed, it SHOULD set incomplete: true and still return events it found.
Shell Behavior
- The shell MUST resolve relay plans according to NIP-65 relay list metadata when available.
- The shell MUST verify that an event returned by
outbox.getEventmatches the requestedeventId. - The shell MUST deduplicate events by event ID before returning
outbox.query.result. - The shell MUST validate event signatures before delivering events to napplets.
- The shell MUST sign event templates submitted through
outbox.publish; napplets do not receive signing keys. - The shell MUST publish at most one signed event for each
outbox.publishrequest. - The shell MUST include the shell-user's NIP-65 write relays in
outbox.publishfanout unlessOutboxPublishOptions.toOutboxisfalse. - The shell MUST resolve NIP-65 read relays for every author in
OutboxPublishOptions.toInboxeswhentoInboxesis non-empty. - The shell MUST include every resolved, policy-allowed
toInboxesrelay inoutbox.publishfanout or report failure throughOutboxPublishResult. - The shell MUST deduplicate
outbox.publishrelay URLs across own-outbox relays, inbox relays, and validated explicit relay URLs before publishing. - The shell MUST respond to every request with a result or lifecycle message carrying the same
idorsubId. - The shell MUST deliver matching subscription events as
outbox.eventuntil the napplet sendsoutbox.closeor the shell terminates the stream withoutbox.closed. - The shell SHOULD merge observed relay URLs into
RelayEventResult.sidecar.relayHintsafter deduplication when it can disclose them. - The shell MAY include pre-resolved byte resources in
RelayEventResult.sidecar.resources; NAP-RELAY's default-off sidecar privacy policy and NAP-RESOURCE's fetch policy both apply. - The shell SHOULD cache relay lists and refresh them according to shell policy.
- The shell SHOULD fall back to configured relays when NIP-65 data is absent, stale, or unreachable, for read targets and for the shell-user's own write set. Fallback never substitutes for a
toInboxesread relay. - The shell MAY use NIP-66 or other relay intelligence to choose among candidate relays.
- The shell MAY enforce ACL checks for outbox read, subscribe, publish, relay override, and relay discovery operations.
Security Considerations
- Relay selection leaks user interest. Shells SHOULD limit broad outbox queries and apply user or napplet-specific policy for sensitive authors and filters.
options.relaysMUST be subject to shell validation. Napplets MUST NOT be able to force connections to private network relays or disallowed hosts.- Event ID lookups without an author can become broad relay searches. Shells SHOULD enforce timeouts, fallback limits, and policy checks before expanding beyond hinted or known relays.
- Unbounded author sets, missing limits, and broad subscriptions can exhaust shell and relay resources. Shells SHOULD enforce limits, timeouts, and maximum subscription counts.
- Publishing remains shell-mediated. Shells SHOULD show consent UI or require prior policy approval before signing and publishing events.
- Relay plans may reveal user relay preferences. Shells MAY redact relay URLs or return a policy-derived plan when a napplet is not trusted to inspect relay metadata.
References
Implementations
- (none yet)
Changelog
a1968b8- Introduced NAP-OUTBOX for outbox-aware relay routing.20379e8- Added single-event retrieval throughoutbox.getEvent.57db924- Changed outbox event returns to use relay-owned result shape and declared the relay dependency.70515a5- Removedstrategy,live, andoutbox.eosecaller-visible controls from outbox options, messages, and examples.e575906- Restored theOutboxSubscriptionhandle definition forevent,closed, andclose()lifecycle methods.0648a60- Replaced inferred publish routing with explicittoOutbox,toInboxes, and validatedrelaysfanout.e46a61a- Adopted injected-domain availability and linked the current upstream web binding.