@napplet/core
JSON envelope types and NAP dispatch infrastructure for the napplet ecosystem.
@napplet/core is the single source of truth for all protocol-level definitions. Every other @napplet/* package imports its envelope types, dispatch infrastructure, and protocol constants from here. It has zero dependencies and no DOM or browser APIs — it works in any JavaScript runtime.
- npm:
@napplet/core - JSR:
@napplet/core - Source: packages/core
Implementation maturity
NAP implementation maturity and upstream proposal status are separate release-time signals. All capabilities remain subject to change; supported does not mean frozen, and experimental does not waive conformance requirements. Read the maturity and opt-in guide.
import { NAP_IMPLEMENTATIONS, selectNapDomains } from '@napplet/core';
console.log(NAP_IMPLEMENTATIONS.llm.maturity); // exploratory
const selection = selectNapDomains({
candidates: ['storage', 'llm', 'ble'], // already implemented and authorized by the host
optIn: ['llm'],
});
// selection.domains: ['storage', 'llm']; selection.excluded explains omissions.Metadata describes the installed package, not a remote shell's protocol revision. Use the selected domains for both runtime injection and host dispatch. Unknown or unclassified candidates remain excluded, including explicit opt-ins. The catalog does not grant permissions or implement missing host support.
Install
npm install @napplet/coreKey exports
import {
type NappletMessage, type NapDomain, type NappletGlobal,
type NapHandler, type NapDispatch,
type IntentOpenOptions, type IntentRequest, type IntentResult,
NAP_DOMAINS, SHELL_BRIDGE_URI, PROTOCOL_VERSION,
createDispatch, registerNap, dispatch, getRegisteredDomains,
ALL_CAPABILITIES, TOPICS,
} from '@napplet/core';Envelope types
NappletMessage— base interface for every message:{ type: string }indomain.actionformat. Concrete message types extend it with payload fields.NapDomain— string literal union of the NAP capability domains ('relay' | 'identity' | 'storage' | 'inc' | 'theme' | 'keys' | 'media' | 'notify' | 'config' | 'resource' | 'cvm' | 'outbox' | 'upload' | 'intent' | 'ble' | 'webrtc' | 'link' | 'count' | 'lists' | 'serial' | 'fs' | 'common' | 'dm' | 'shell' | 'gamepad' | 'llm').NappletGlobal— the runtime-injectedwindow.nappletnamespace with optional domain properties.NAP_DOMAINS— runtime constant array of all domain strings, for iteration and validation.
Dispatch infrastructure
NAP modules self-register at import time; inbound messages route by the domain prefix of message.type (the part before the first .).
createDispatch()— factory returning an isolated{ registerNap, dispatch, getRegisteredDomains }backed by its own registry.registerNap(domain, handler)— register a handler on the module-level singleton registry (throws if the domain is already registered).dispatch(message)— route a message to its handler; returnstrueif one was found and called,falsefor an unknown or malformed type.getRegisteredDomains()— list registered domain strings.
Boundary helpers (clone-safety)
Every NAP shim crosses the napplet ⇄ shell boundary by structured-cloning a JSON envelope through postMessage. Framework reactive values — Svelte 5 $state, Vue reactive, Solid stores — are Proxy objects that are not structured- cloneable, so a naive postMessage throws DataCloneError, which is silently swallowed in async paths (the envelope never crosses). These helpers fix that.
sendEnvelope(target, message, targetOrigin?)— the single boundary chokepoint the shims post through. Per the active clone mode it posts the envelope, recovers a non-cloneable argument via a snapshot, or throws a loud, actionable, synchronous error instead of swallowing it.toCloneableSnapshot(value)— deep snapshot that strips reactive proxies into plain objects/arrays while preserving binary (Uint8Array,ArrayBuffer),Date,RegExp,Map,Set, and cycles. Unlike aJSONround-trip it is lossless for binary; functions/symbols throw (never masked).setCloneMode(mode)/getCloneMode()— choose how arguments are treated:'auto'(default — post as-is, snapshot-and-retry onDataCloneError, warn once),'strict'(throw onDataCloneError, never recover), or'snapshot'(eagerly snapshot every envelope).clearCloneWarnings()— reset the once-per-type auto-recovery warnings.
import { setCloneMode, toCloneableSnapshot } from '@napplet/core';
// Default 'auto' just works — reactive state is snapshotted on the failure path. napplet.outbox.subscribe(filters, { relays, timeoutMs: 3000 });
// Or normalize explicitly / eagerly:
napplet.outbox.subscribe(toCloneableSnapshot(filters), { relays });
setCloneMode('snapshot');These helpers are SDK plumbing only — the bytes placed on the wire are identical plain envelopes, so they add no protocol surface.
Protocol types & constants
NostrEvent,NostrFilter,EventTemplate,Subscription— shared Nostr structures used by NAP domains such as outbox, relay, and identity.Capability/ALL_CAPABILITIES— the human-readable capability strings (relay:read,relay:write,sign:event,sign:nip44,state:read, …).PROTOCOL_VERSION('4.0.0'),SHELL_BRIDGE_URI('napplet://shell'),REPLAY_WINDOW_SECONDS(30), and the legacyTOPICSrouting constants.
Convention boundaries
The stable identity is the complete, queryless napplet:<archetype>/<intent> string. Archetype manifest tags advertise accepted queryless convention identities. Routing and handler resolution use exact equality.
INC emit(topic, payload?) accepts a convention URI. Unique percent-decoded query pairs become a shallow string-to-string payload before postMessage; literal + remains +. The normalized topic is queryless:
napplet.inc.emit('napplet:profile/open?pubkey=abc123');
napplet.inc.on('napplet:profile/open', (event) => validateLocally(event.payload));
const result = await napplet.intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } });
if (!result.ok) throw new Error(result.error);Intent calls derive archetype and action from the URI. An ok: true result means the runtime accepted delivery responsibility and includes the normalized identity and handler catalog identifier; an ok: false result includes error. Targets receive runtime-attested IntentDelivery values through onDelivery, including deliveries retained before registration. Behavior hints are focus and reuse. This non-normative guidance defers to the living NAP-INTENT.
This package defers to the living NAP-INC and NAP-INTENT documents.
Usage
Register a handler and dispatch a message through an isolated registry:
import { createDispatch } from '@napplet/core';
const { registerNap, dispatch } = createDispatch();
registerNap('outbox', (msg) => { // handles all outbox.* messages console.log('outbox message:', msg.type); });
dispatch({ type: 'outbox.query', id: 'abc', filters: [{ kinds: [1] }] }); // true
dispatch({ type: 'unknown.action' }); // false
dispatch({ type: 'malformed' }); // false (no dot)See also
- Core concepts — the JSON envelope and NAP dispatch model
- NIP-5D explained — the wire format this package implements
Optional shell environment
The runtime may expose shell for environment information. shell.supports(domain) reads current domain object presence, shell.services is empty until environment delivery, and shell.ready() / shell.onReady(handler) use the retained snapshot. Readiness does not gate other domain calls. See the living NAP-SHELL.
Intent delivery and recommendations
import { intent } from '@napplet/sdk';
intent.onDelivery(({ sender, convention, payload }) => {
console.log(sender, convention, payload);
});
await intent.invoke('napplet:profile/open?pubkey=abc123');The runtime binding retains deliveries until a listener registers. Discovery candidates expose opaque catalog id values and contracts with parameter names. INC peers instead use identifiers for running authenticated endpoints. A NAP-INTENT URI may append a bare #naddr1… recommendation; the binding sends its coordinate and relay hints as handlerHint, outside convention identity and payload. An applicable user default takes precedence. INC URI operations reject fragments. See the living NAP-INTENT and web projection.
LLM types
LlmApi and the LLM records describe NappletGlobal.llm, an optional domain for user-selected, runtime-mediated language model inference: connection selection and revalidation, stateless generation with tool definitions, content parts, and optional block-lifecycle deltas, and cancellation. LlmErrorCode lists the specification categories while staying open, because unknown codes remain failures. See the non-normative LLM guide and living NAP-LLM proposal.