napplet.
← All NAPs

NAP-HASHTREE / PR #73

Runtime-Mediated Hashtree Document Trees

Draft PR

Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.

NAP-HASHTREE

Runtime-Mediated Hashtree Document Trees

draft

NAP ID: NAP-HASHTREE Domain: hashtree Depends:

  • identity -- capability · required -- publishing mutable Hashtree roots uses the current shell-user identity as event author.
  • relay -- capability · required -- resolves and publishes Hashtree root events through the runtime relay surface.
  • relay -- wire · required -- imports RelayEventResult for resolved mutable-root event returns.
  • resource -- wire · optional -- imported RelayEventResult.sidecar.resources? carries ResourceSidecarEntry[], a type owned by the resource domain. Web binding (NIP-5D): window.napplet.hashtree; domain presence signals availability.

Description

NAP-HASHTREE gives napplets a runtime-mediated interface for Hashtree document trees. A napplet can store bytes, create directories, resolve immutable nhash CIDs, resolve mutable htree://<identifier>/<path> roots, read files, list directories, publish root events, and manage pins/import/export jobs.

Hashtree's upstream protocol is implemented and has a draft spec (HTS-01). This NAP treats HTS-01 as the source for Hashtree bytes: SHA-256 content addresses, deterministic MessagePack tree nodes, CHK encryption, nhash identifiers, Nostr kind 30064 mutable roots, and htree:// URLs. The NAP defines the napplet-facing runtime API around those concepts.

The runtime owns storage backends, Blossom/FIPS/local-daemon routing, CHK key handling, root-event signing, relay selection, pin retention, filesystem pickers, and user consent. Napplets never receive direct Blossom, FIPS, filesystem, or signing access through this interface.

API Surface

Operation Parameters Result Wire
capabilities none HashtreeCapabilities hashtree.capabilities / .result
parseRef ref (tstr) HashtreeRefResult hashtree.parseRef / .result
resolve ref (tstr), optional options (HashtreeResolveOptions) HashtreeResolveResult hashtree.resolve / .result
stat ref (tstr) HashtreeEntryResult hashtree.stat / .result
list ref (tstr), optional options (HashtreeListOptions) HashtreeListResult hashtree.list / .result
read ref (tstr), optional options (HashtreeReadOptions) HashtreeReadResult hashtree.read / .result
putBytes request (HashtreePutBytesRequest) HashtreePutResult hashtree.putBytes / .result
putTree request (HashtreePutTreeRequest) HashtreePutResult hashtree.putTree / .result
publishRoot request (HashtreePublishRootRequest) HashtreePublishRootResult hashtree.publishRoot / .result
pickFiles optional options (HashtreePickOptions) HashtreeFileSetResult hashtree.pickFiles / .result
importFiles request (HashtreeImportRequest) HashtreeJobAck hashtree.importFiles / .result, then push messages
exportFiles request (HashtreeExportRequest) HashtreeJobAck hashtree.exportFiles / .result, then push messages
pins none list of HashtreePin hashtree.pins / .result
pin request (HashtreePinRequest) HashtreePinResult hashtree.pin / .result
unpin ref (tstr) HashtreePinResult hashtree.unpin / .result
job jobId (tstr) HashtreeJobStatus hashtree.job / .result
cancel jobId (tstr) HashtreeJobStatus hashtree.cancel / .result

Schemas

RelayEventResult is owned by NAP-RELAY. NAP-HASHTREE imports it by name for root events returned while resolving mutable Hashtree references. publishRoot still returns its shell-created signed event as NostrEvent.

Primitive aliases and imported types:

Name Definition
HexHash hash encoded as lowercase hex
HexKey key material encoded as lowercase hex
HexPubkey 32-byte public key encoded as lowercase hex
NostrEvent Nostr event object
NostrEventId 32-byte event id encoded as lowercase hex
RelayEventResult external type owned by NAP-RELAY

Enums:

Name Values
HashtreeRefKind nhash, cid, htree, npub-path
HashtreeEntryType blob, file, dir
HashtreeVisibility unencrypted, public, link-visible, private
HashtreeJobKind import, export
HashtreeJobState queued, running, complete, cancelled, error
HashtreeExportDestination user-selected, temporary

Record fields:

Record Required fields Optional fields
HashtreeCapabilities none canReadImmutable: bool, canResolveMutable: bool, canWrite: bool, canPublishRoots: bool, canPin: bool, canImportRuntimeFiles: bool, canExportRuntimeFiles: bool, supportsFips: bool, supportsBlossom: bool, supportsLocalStore: bool, maxReadBytes: uint, maxWriteBytes: uint
HashtreeCid hash: HexHash key: HexKey, nhash: tstr
HashtreeRef kind: HashtreeRefKind ref: tstr, cid: HashtreeCid, identifier: tstr, treePath: tstr, entryPath: tstr, visibility: HashtreeVisibility, shareSecret: HexKey
HashtreeRefResult ok: bool ref: HashtreeRef, error: tstr, reason: tstr
HashtreeResolveOptions none shareSecret: HexKey, at: uint
HashtreeResolvedRoot cid: HashtreeCid ref: HashtreeRef, rootResult: RelayEventResult, eventId: NostrEventId, author: HexPubkey, treeName: tstr, visibility: HashtreeVisibility
HashtreeResolveResult ok: bool root: HashtreeResolvedRoot, error: tstr, reason: tstr
HashtreeEntry path: tstr, type: HashtreeEntryType, cid: HashtreeCid name: tstr, size: uint, mime: tstr
HashtreeEntryResult ok: bool entry: HashtreeEntry, error: tstr, reason: tstr
HashtreeListOptions none recursive: bool, limit: uint
HashtreeListResult ok: bool, entries: list of HashtreeEntry root: HashtreeResolvedRoot, truncated: bool, error: tstr, reason: tstr
HashtreeReadOptions none offset: uint, length: uint, maxBytes: uint
HashtreeReadResult ok: bool blob: bstr, entry: HashtreeEntry, root: HashtreeResolvedRoot, error: tstr, reason: tstr
HashtreePutOptions none visibility: HashtreeVisibility, mime: tstr, filename: tstr, pin: bool
HashtreePutBytesRequest data: bstr options: HashtreePutOptions
HashtreeTreeEntryInput path: tstr data: bstr, cid: HashtreeCid, mime: tstr
HashtreePutTreeRequest entries: list of HashtreeTreeEntryInput options: HashtreePutOptions
HashtreePutResult ok: bool cid: HashtreeCid, entry: HashtreeEntry, size: uint, error: tstr, reason: tstr
HashtreePublishRootRequest cid: HashtreeCid, treeName: tstr content: tstr, visibility: HashtreeVisibility, shareSecret: HexKey, relays: list of tstr
HashtreePublishRootResult ok: bool eventId: NostrEventId, event: NostrEvent, ref: tstr, relays: map of relay URL to bool, error: tstr, reason: tstr
HashtreeImportPublishOptions treeName: tstr content: tstr, visibility: HashtreeVisibility, shareSecret: HexKey, relays: list of tstr
HashtreePickOptions none multiple: bool, directory: bool, suggestedName: tstr
HashtreeRuntimeFileSet runtimeFileSetId: tstr, files: list of HashtreeEntry, totalBytes: uint displayName: tstr
HashtreeFileSetResult ok: bool fileSet: HashtreeRuntimeFileSet, error: tstr, reason: tstr
HashtreeImportRequest runtimeFileSetId: tstr visibility: HashtreeVisibility, pin: bool, publish: HashtreeImportPublishOptions
HashtreeExportRequest ref: tstr destination: HashtreeExportDestination
HashtreeJobAck ok: bool jobId: tstr, state: HashtreeJobState, error: tstr, reason: tstr
HashtreeJobStatus ok: bool, jobId: tstr, kind: HashtreeJobKind, state: HashtreeJobState, processedBytes: uint, totalBytes: uint, progress: number cid: HashtreeCid, ref: tstr, runtimeFileSetId: tstr, error: tstr, reason: tstr
HashtreePin ref: tstr, cid: HashtreeCid label: tstr, bytesStored: uint, createdAt: uint
HashtreePinRequest ref: tstr label: tstr
HashtreePinResult ok: bool pin: HashtreePin, error: tstr, reason: tstr

When HashtreePutOptions.visibility is omitted, the default is public.

capabilities() -- Reports the runtime's Hashtree support for this napplet. Backend flags describe runtime policy, not direct napplet access.

parseRef(ref) -- Parses nhash, CID text (<hash> or <hash>:<key>), htree://, and npub/path references without fetching data. hashtree:nhash MUST parse as the same immutable reference as nhash.

resolve(ref, options?) -- Resolves immutable or mutable references to a root CID. For mutable references, the runtime queries Nostr root events, chooses the newest root using HTS-01 ordering, and derives public, link-visible, or private root keys when authorized. Returned mutable-root events use RelayEventResult.

stat(ref) -- Returns metadata for a blob, file, or directory without returning file bytes.

list(ref, options?) -- Lists directory entries. The runtime decodes and verifies Hashtree nodes before returning entries. recursive can be capped by runtime policy.

read(ref, options?) -- Reads a blob or file. The runtime fetches chunks from local storage, Blossom, FIPS peers, or another configured backend, verifies hashes, decrypts when a key is available, and returns bytes. It MUST reject a directory read with "is-directory".

putBytes(request) -- Stores bytes as a Hashtree blob/file and returns its CID. The runtime applies HTS-01 chunking, node encoding, and optional CHK encryption. It MAY upload stored objects to configured remote stores.

putTree(request) -- Builds a Hashtree directory from supplied bytes and/or existing CIDs. Paths are logical tree paths, not filesystem paths. The runtime normalizes and rejects traversal.

publishRoot(request) -- Publishes a Nostr kind 30064 root event with d, l, and hash tags, plus the appropriate visibility tag. The runtime signs as the current shell-user and owns relay fanout.

pickFiles(options?) -- Opens a runtime-owned file or directory picker and returns an opaque runtimeFileSetId. The handle can be imported, but it does not expose local paths or bytes to the napplet.

importFiles(request) -- Stores a runtime-owned file set as a Hashtree tree. It is a long-running job because large directories may need chunking, hashing, uploading, pinning, and optional root publication.

exportFiles(request) -- Materializes a Hashtree ref into a runtime-owned file set or user-selected destination. It is a long-running job because it may need network fetches, verification, decryption, and directory reconstruction.

pins() / pin(request) / unpin(ref) -- Manage runtime retention for Hashtree CIDs. Pinning is a storage policy request, not a publication event.

job(jobId) / cancel(jobId) -- Inspect or cancel import/export jobs.

HTS-01 Mapping

HTS-01 concept NAP-HASHTREE field or behavior
SHA-256 content address HashtreeCid.hash
CID key HashtreeCid.key
nhash HashtreeCid.nhash and immutable parseRef output
MessagePack file/dir node runtime-owned implementation behind putTree, list, and read
CHK encryption visibility, key, shareSecret, and runtime encryption policy
kind 30064 root event publishRoot, resolve, HashtreeResolvedRoot.rootResult
d tag treeName
hash tag HashtreeCid.hash
key, encryptedKey, selfEncryptedKey tags root visibility and key recovery
htree://<identifier>/<path> mutable HashtreeRef

htree:// tree names that contain / MUST follow HTS-01 URL encoding guidance: encode the tree name as one path segment before appending entry path segments.

Wire Protocol

hashtree.* messages use the NIP-5D wire format ({ "type": "domain.action", ...payload }).

Type Direction Payload fields
hashtree.capabilities napplet -> runtime id
hashtree.capabilities.result runtime -> napplet id, capabilities?, error?
hashtree.parseRef napplet -> runtime id, ref
hashtree.parseRef.result runtime -> napplet id, result
hashtree.resolve napplet -> runtime id, ref, options?
hashtree.resolve.result runtime -> napplet id, result
hashtree.stat napplet -> runtime id, ref
hashtree.stat.result runtime -> napplet id, result
hashtree.list napplet -> runtime id, ref, options?
hashtree.list.result runtime -> napplet id, result
hashtree.read napplet -> runtime id, ref, options?
hashtree.read.result runtime -> napplet id, result
hashtree.putBytes napplet -> runtime id, request
hashtree.putBytes.result runtime -> napplet id, result
hashtree.putTree napplet -> runtime id, request
hashtree.putTree.result runtime -> napplet id, result
hashtree.publishRoot napplet -> runtime id, request
hashtree.publishRoot.result runtime -> napplet id, result
hashtree.pickFiles napplet -> runtime id, options?
hashtree.pickFiles.result runtime -> napplet id, result
hashtree.importFiles napplet -> runtime id, request
hashtree.importFiles.result runtime -> napplet id, ack
hashtree.exportFiles napplet -> runtime id, request
hashtree.exportFiles.result runtime -> napplet id, ack
hashtree.pins napplet -> runtime id
hashtree.pins.result runtime -> napplet id, pins?, error?
hashtree.pin napplet -> runtime id, request
hashtree.pin.result runtime -> napplet id, result
hashtree.unpin napplet -> runtime id, ref
hashtree.unpin.result runtime -> napplet id, result
hashtree.job napplet -> runtime id, jobId
hashtree.job.result runtime -> napplet id, status?, error?
hashtree.cancel napplet -> runtime id, jobId
hashtree.cancel.result runtime -> napplet id, status?, error?
hashtree.progress runtime -> napplet jobId, status
hashtree.done runtime -> napplet jobId, status
hashtree.error runtime -> napplet jobId, error, reason?

Key design notes:

  • Request/result pairs use id for correlation.
  • Import/export push messages use jobId; they have no id.
  • Binary data and blob fields cross the boundary as projection-native byte objects. In the web projection that means Blob or ArrayBuffer, not base64 strings.
  • runtimeFileSetId is runtime-scoped. It is not a path and cannot be reused outside the grant that produced it.
  • Root publication and resolution are part of this domain. Generic Nostr relay access remains in NAP-RELAY.

Examples

Resolve and read a public file:

-> { "type": "hashtree.resolve", "id": "r1", "ref": "htree://npub1.../docs/readme.txt" }
<- { "type": "hashtree.resolve.result", "id": "r1",
     "result": { "ok": true, "root": { "cid": { "hash": "ab12..." }, "treeName": "docs", "visibility": "unencrypted" } } }
-> { "type": "hashtree.read", "id": "r2", "ref": "htree://npub1.../docs/readme.txt" }
<- { "type": "hashtree.read.result", "id": "r2",
     "result": { "ok": true, "blob": "<bytes>", "entry": { "path": "readme.txt", "type": "file", "cid": { "hash": "cd34..." }, "size": 1200, "mime": "text/plain" } } }

Store bytes and publish a root:

-> { "type": "hashtree.putBytes", "id": "p1",
     "request": { "data": "<bytes>", "options": { "visibility": "public", "filename": "note.txt", "pin": true } } }
<- { "type": "hashtree.putBytes.result", "id": "p1",
     "result": { "ok": true, "cid": { "hash": "ab12...", "key": "ef56...", "nhash": "nhash1..." }, "entry": { "path": "note.txt", "type": "file", "cid": { "hash": "ab12...", "key": "ef56..." }, "size": 512 } } }
-> { "type": "hashtree.publishRoot", "id": "p2",
     "request": { "cid": { "hash": "ab12...", "key": "ef56..." }, "treeName": "notes", "visibility": "public" } }
<- { "type": "hashtree.publishRoot.result", "id": "p2",
     "result": { "ok": true, "eventId": "event...", "ref": "htree://npub1.../notes" } }

Import a user-selected directory:

-> { "type": "hashtree.pickFiles", "id": "f1", "options": { "directory": true, "suggestedName": "site" } }
<- { "type": "hashtree.pickFiles.result", "id": "f1",
     "result": { "ok": true, "fileSet": { "runtimeFileSetId": "fs-1", "files": [], "totalBytes": 7340032, "displayName": "site" } } }
-> { "type": "hashtree.importFiles", "id": "i1",
     "request": { "runtimeFileSetId": "fs-1", "visibility": "link-visible", "pin": true,
                  "publish": { "treeName": "site", "visibility": "link-visible" } } }
<- { "type": "hashtree.importFiles.result", "id": "i1", "ack": { "ok": true, "jobId": "ht-1", "state": "queued" } }
<- { "type": "hashtree.done", "jobId": "ht-1",
     "status": { "ok": true, "jobId": "ht-1", "kind": "import", "state": "complete", "processedBytes": 7340032, "totalBytes": 7340032, "progress": 1, "cid": { "hash": "ab12...", "key": "ef56..." }, "ref": "htree://npub1.../site#k=..." } }

Error Handling

Common errors: invalid-ref, invalid-cid, invalid-tree, invalid-path, not-found, missing-key, decrypt-failed, unsupported-visibility, not-signed-in, user-denied, relay-timeout, publish-failed, fetch-failed, store-failed, too-large, quota-exceeded, is-directory, not-directory, file-pick-cancelled, job-not-found, policy-denied, unsupported.

The runtime SHOULD return structured ok: false results when the request was understood but refused. It SHOULD use top-level error only when no typed result can be constructed.

Runtime Behavior

  • MUST validate all returned bytes by SHA-256 before delivering them.
  • MUST reject deterministic MessagePack tree nodes that violate HTS-01 ordering or type rules.
  • MUST keep CHK keys, link-visible share secrets, and private root keys under runtime policy.
  • MUST construct kind 30064 root events with d, l, and hash tags.
  • MUST sign root events as the current shell-user.
  • MUST choose relays, Blossom servers, FIPS peers, local caches, and fallback order by runtime policy.
  • SHOULD merge observed mutable-root relay URLs into RelayEventResult.sidecar.relayHints when it can disclose them.
  • MAY include pre-resolved resources referenced by mutable-root events in RelayEventResult.sidecar.resources; NAP-RELAY's default-off sidecar privacy policy and NAP-RESOURCE's fetch policy both apply.
  • MUST NOT expose raw Blossom, FIPS, filesystem, or signing access through this interface.
  • MUST sanitize logical paths and reject absolute paths, .., and equivalent traversal.
  • MUST scope pins, jobs, and runtime file handles to the requesting napplet unless the user explicitly grants broader access.
  • SHOULD cap recursive listing, read size, import size, export size, and concurrent jobs.
  • MAY support legacy root kind 30078 when resolving, as HTS-01 permits.

Security Considerations

  • A CID with a CHK key is a read capability. Runtimes SHOULD avoid exposing keys unless needed by the napplet and authorized by the user.
  • Link-visible roots carry a share secret in a fragment. Runtimes MUST NOT send that secret to relays, Blossom servers, FIPS peers, or HTTP request bodies.
  • Private roots require user identity and NIP-44 decryption. Runtimes MUST reject private resolution when no authorized signer/decrypter is connected.
  • Importing local files can exfiltrate user data. Consent UI SHOULD show file count, approximate bytes, visibility mode, publication target, and retention policy before starting.
  • Exporting can materialize untrusted filenames. Runtimes MUST sanitize names and prevent traversal outside the chosen destination.
  • FIPS and Blossom fetches can leak interest. Runtimes SHOULD disclose network path policy when resolving remote roots.
  • Pins consume storage and may retain sensitive encrypted blobs. Runtimes SHOULD make pin scope and retention visible.

References

Implementations

  • None yet.

Changelog

  • 08c0396 - Introduced NAP-HASHTREE for runtime-mediated Hashtree document trees.

  • 3847b0f - Changed resolved Hashtree root events to use relay-owned result shape and declared the relay dependency.

  • 623b44c - Adopted injected-domain availability and linked the current upstream web binding.