NAP-STORAGE / PR #3
Scoped Key-Value Storage
Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.
NAP-STORAGE
Scoped Key-Value Storage
draft
NAP ID: NAP-STORAGE
Domain: storage
Depends: none.
Web binding (NIP-5D): window.napplet.storage; domain presence signals availability.
Description
NAP-STORAGE provides napplets with an async localStorage-like API. Without allow-same-origin, iframes have opaque origins and cannot access localStorage directly. This interface routes storage operations through the shell via postMessage, which scopes data by napplet identity — a verified manifest and artifact scope — to enforce isolation between napplets. Different napplet types and different versions of the same napplet have completely separate storage namespaces.
API Surface
| Operation | Parameters | Result | Wire |
|---|---|---|---|
getItem |
key (tstr) |
tstr or null |
storage.get / storage.get.result |
setItem |
key (tstr), value (tstr) |
none | storage.set / storage.set.result |
removeItem |
key (tstr) |
none | storage.remove / storage.remove.result |
keys |
none | list of tstr keys |
storage.keys / storage.keys.result |
instance.getItem |
key (tstr) |
tstr or null |
storage.get / storage.get.result with scope: "instance" |
instance.setItem |
key (tstr), value (tstr) |
none | storage.set / storage.set.result with scope: "instance" |
instance.removeItem |
key (tstr) |
none | storage.remove / storage.remove.result with scope: "instance" |
instance.keys |
none | list of tstr keys |
storage.keys / storage.keys.result with scope: "instance" |
getItem(key) — Retrieves a stored value by key. Returns null if the key does not exist, matching localStorage semantics.
setItem(key, value) — Stores a key-value pair. Throws if the napplet exceeds its storage quota.
removeItem(key) — Deletes a stored key. No-op if the key does not exist.
keys() — Lists all keys stored by this napplet.
All methods are async because they cross the postMessage boundary. Each request includes a correlation ID (id); the shell responds with a matching ID so the napplet can resolve the correct Promise.
instance — The same four methods, scoped to the calling instance instead of shared across all instances of the napplet. A napplet open more than once (e.g. several feeds) uses instance.* for per-window state and the top-level methods for shared state. Scope is per call: instancing is a property of the data, not the napplet. See Instance Scope.
Identity Scope
The runtime MUST derive scope from the authenticated endpoint's verified
manifest and artifact. For a named manifest, the manifest key is its publisher,
kind, and d value; for a root manifest, publisher and kind; for a snapshot,
its signed event id. The scope also includes artifactHash, the verified
single-artifact hash defined by NIP-5D.
A bare d value MUST NOT identify a scope. Root and snapshot manifests require
no d tag. A napplet MUST NOT supply or override manifest or artifact identity fields in a request.
Different publishers, manifest keys, and artifact hashes MUST remain isolated.
Wire Protocol
Storage operations use the NIP-5D wire format. Each request includes an id field for correlation.
Every request carries an optional scope: "shared" (default) or "instance". It is the only wire-level addition for per-instance storage; the instance.* API is sugar that sets scope: "instance".
| Type | Direction | Payload fields |
|---|---|---|
storage.get |
napplet -> shell | id, key, scope? |
storage.set |
napplet -> shell | id, key, value, scope? |
storage.remove |
napplet -> shell | id, key, scope? |
storage.keys |
napplet -> shell | id, scope? |
storage.get.result |
shell -> napplet | id, value (string or null) |
storage.set.result |
shell -> napplet | id |
storage.remove.result |
shell -> napplet | id |
storage.keys.result |
shell -> napplet | id, keys (string array) |
Result messages carry no scope; the correlation id already identifies the request.
Examples
Get:
-> { "type": "storage.get", "id": "a1", "key": "theme" }
<- { "type": "storage.get.result", "id": "a1", "value": "dark" }
If the key does not exist, value is null:
-> { "type": "storage.get", "id": "a2", "key": "missing-key" }
<- { "type": "storage.get.result", "id": "a2", "value": null }
Set:
-> { "type": "storage.set", "id": "b2", "key": "theme", "value": "light" }
<- { "type": "storage.set.result", "id": "b2" }
Remove:
-> { "type": "storage.remove", "id": "c3", "key": "theme" }
<- { "type": "storage.remove.result", "id": "c3" }
Keys:
-> { "type": "storage.keys", "id": "d4" }
<- { "type": "storage.keys.result", "id": "d4", "keys": ["theme", "language"] }
Any request MAY add "scope": "instance" to address the calling instance's namespace instead of the shared one.
Error Handling
Any result message MAY include an error field (string). When error is present, other result fields are undefined.
<- { "type": "storage.set.result", "id": "b2", "error": "quota exceeded" }
Instance Scope
When a napplet is open more than once (e.g. several feeds), scope: "instance"
isolates a key to the calling instance while scope: "shared" (the default)
stays common to every instance. The choice is per call, so per-instance state
(criteria, scroll) and shared state (theme, preferences) coexist without a
napplet-wide mode.
The instance namespace is defined by two guarantees:
- Unique — instances MUST NOT share an instance namespace or read each other's instance-scoped keys.
- Stable — the same instance MUST resolve to the same namespace across reloads and sessions, for as long as it exists.
How the shell identifies an instance and keeps it stable is unspecified —
the shell's concern. A napplet never sees or names an instance identifier; it
sets scope and trusts the two guarantees.
Lifetime. Instance storage lives as long as the instance; the shell MAY
reclaim it on destroy. State that must outlive the instance belongs in shared.
Shell Behavior
- The shell MUST scope storage by the verified manifest and artifact scope. Different napplet types and different versions of the same napplet MUST have isolated storage. The shell maps each napplet's verified manifest and artifact scope to an isolated storage namespace.
- For
scope: "instance"requests, the shell MUST further isolate the namespace per instance, satisfying the Unique and Stable guarantees above. Requests withscopeabsent or"shared"MUST address the napplet-wide namespace, preserving the behavior of napplets that never setscope. - The shell MUST enforce a per-napplet storage quota. A recommended default is 512 KB measured in UTF-8 byte count. Whether instance namespaces draw from the same per-napplet budget or a per-instance budget is an implementation detail.
- The shell MUST persist storage so data survives page reloads. How the shell stores data (localStorage, IndexedDB, etc.) is an implementation detail.
- The shell MUST respond to every request with a result message carrying the same
id. - The shell MUST return an
errorfield in the result message for quota violations and invalid requests.
Security Considerations
- Storage uses the verified manifest and artifact scope. The shell MUST enforce isolation so a napplet cannot read or write another napplet's data. For
scope: "instance", the shell MUST additionally enforce isolation between instances of the same napplet. - Storage quota enforcement prevents a single napplet from consuming unbounded host storage.
- Storage values are strings only. The shell SHOULD NOT attempt to parse or execute stored content.
- The shell MAY enforce ACL checks on
storage:readandstorage:writecapabilities before processing storage requests. - Storage keys and values are visible to the shell. Napplets that need confidential storage should encrypt values before storing.
Changelog
fe4a523- Introduced NAP-STORAGE for scoped key-value storage.811f011- Adopted injected-domain availability and verified manifest/artifact scopes for all napplet kinds, with publisher isolation.