@napplet/conformance
Framework-agnostic conformance engine for the napplet protocol — lets a napplet self-verify that it conforms to NIP-5D + the NAP wire protocol before publishing.
@napplet/conformance is a development/testing tool. It is not loaded inside the napplet sandbox at runtime. One engine drives two scopes:
- Headless / CI — via the
@napplet/conformance-clinapplet-conformancePlaywright runner. - Standalone web runtime — the single-window
apps/conformanceapp.
v1 is zero-config protocol conformance: NIP-5D manifest-event validity, boots under sandbox="allow-scripts", receives a runtime-injected window.napplet, every emitted postMessage envelope validates against the per-NAP validators, graceful degradation when a domain is absent, and no forbidden globals.
The harness acts as a NIP-5D shell for the port-bound identity model: it binds the MessagePort from the first valid napplet.hello, answers only over that port, and revokes the napplet and removes its frame on a second hello, any other window-level message, a second load or the port closing. wire/port-only fails a napplet that posts messages at window level instead of through window.napplet; lifecycle/keeps-verified-document is an advisory for a napplet that navigates or reloads its own frame. attachReferenceShell(shell, { host, expectedSource }) and bindNappletPort give custom harnesses the same binding.
- npm:
@napplet/conformance - JSR:
@napplet/conformance - Source: packages/conformance
Local maturity policy
The reference mock can enforce a resolved host domain ceiling: createReferenceShell({ domains: selection.domains }), where selection comes from selectNapDomains in @napplet/core. The same set belongs in the injected prelude. Per-endpoint grants can narrow this ceiling but cannot widen it; excluded requests remain recorded for envelope validation and receive no response.
bootAndCollect({ ...options, domains: ['storage', 'llm'], maturity: {} }) selects supported candidates before boot and uses that set in both injection and dispatch. A named opt-in such as maturity: { optIn: ['llm'] } enables evaluation of that candidate. Normal conformance runs omit maturity filtering and retain full-domain coverage. Experimental status never waives protocol validation. See the maturity guide.
Install
npm install -D @napplet/conformanceWhat's in the box
validateEnvelope(msg)— runtime validation of anydomain.actionenvelope a napplet emits, across every NAP domain. Catches malformed payloads, unknown types, and napplets that put shell→napplet (inbound) traffic on the wire.validateManifestEvent(event)— checks that a resolved Nostr event is a NIP-5D napplet manifest (5129,15129, or35129) with a single/index.htmlartifactxhash, non-empty description, and bareR/Odomains. Domains outside the toolchain registry produce an advisorymanifest/requires-knownwarning without failing the run or implying runtime support.validateManifest(html)— compatibility wrapper for older HTML-only harnesses. HTML alone cannot prove a signed NIP-5D manifest event.parsePresentationMedia(event)— applies NIP-5D §Presentation media toscreenshotandpreviewtags: splits NIP-92key valuefields, requires onexsha256, drops entries clients must ignore, and returns the usable list in manifest order with advisories for everything ignored.- Reference mock shell, check registry, and reporters that the CLI and the web runtime share.
The validator surface is kept in lockstep with @napplet/nap by a drift test, so a new NAP message type cannot ship without matching conformance coverage.
Manifest validation accepts extra Nostr tags and leaves their payloads and multiplicity unchanged. It checks required fields and recognized optional metadata rather than enforcing a tag-name allowlist. A source tag is checked for its single-location shape and supported HTTP(S), SSH, Git, scp-like, or NIP-34 nostr:// format; source-commit and source-archive are checked for their single-tag shape. Invalid icons, screenshots and previews, and a source-commit without a repository source, remain advisory (manifest/media-usable) with fallback. Extra tags cannot make missing content, invalid hashes, or malformed capability declarations pass. This is non-normative implementation guidance; consult the living NIP-5D proposal for the requirements.
import { validateEnvelope, validateManifestEvent } from '@napplet/conformance';
validateEnvelope({
type: 'outbox.query',
id: 'a',
filters: [{ kinds: [1] }],
}).ok; // true
validateManifestEvent(resolvedManifestEvent).ok; // true when the manifest event is well-formedTIP
NIP-5D loads a napplet as a single self-contained /index.html via iframe.srcdoc (opaque origin), so a napplet's JS is inline by design. Conformance does not penalize inline <script> elements.
See also
@napplet/conformance-cli— the headless runner that drives this engine- NIP-5D explained — manifest & NAP negotiation
- Core concepts — the envelope wire format these validators check
Custom browser hosts
bootAndCollect({ url, html, runtimePreludeSource }) requires the maintained shim IIFE source and accepts an optional loaded HTML snapshot. The CLI reads @napplet/shim/prelude.global; Vite hosts can import it with ?raw. The harness appends the explicit domain allowlist call, so the napplet sees exactly the bindings the maintained runtime installs; it ships no bindings of its own. Supplying html avoids refetching an artifact after the host verifies its signature and raw bytes. The engine remains browser-safe and does not import Node APIs or bundled asset loaders. installedGlobal records observed installation independently from bootError; an app failure does not imply injection failed. Signature/hash verification belongs to the loading host, while validateManifestEvent checks the event structure. This is non-normative implementation guidance; consult the living NIP-5D proposal and NAPs.
LLM conformance scope
NAP-LLM validation checks the four request schemas (exact API-row parameters, strict input records without unknown fields or null, enum roles and formats, padded base64 image data, nonempty ids, positive integer maxTokens, portable unique tool names with object-rooted schemas, an explicit toolChoice only with tools and a listed forced name, thinking and toolCall parts only in assistant messages, and each tool call answered exactly once by an adjacent, name-matching toolResult) and rejects result and delta envelopes emitted by napplets, following the living NAP-LLM proposal. The reference shell is a deterministic mock: one connection that supports images, JSON, tools, and every tool-choice mode, endpoint-scoped handles, exact-model requirement matching, a start / delta / end block lifecycle before the authoritative result, echoed output (trailing tool results echoed with the specified Tool execution failed. prefix for errors), a toolUse result for required or named choices, and fixture markers in the last user text ([[decline]], [[refuse]], [[stall]], [[think]], [[tool]]) for a declined prompt, a provider refusal, a generation that stays active until cancel, a display-only thinking block, and a call to the first supplied tool. It never executes a tool call. It returns typed failures rather than fabricated success. A malformed LLM request that still carries a non-empty id gets its .result with an invalid-request error, as the Wire Protocol requires; uncorrelated envelopes and napplet-emitted results are discarded. Passing mock conformance does not prove live inference, consent prompts, provider adapters, or billing behavior; test those in an LLM-capable host.