@napplet/shim
Runtime-side helper for injecting selected
window.napplet.<domain>objects before napplet scripts run.
@napplet/shim is consumed by NIP-5D runtimes, not by napplet application code. The runtime calls its installer before any napplet script runs, injecting only the NAP domain objects exposed to that napplet. It has no cryptographic dependencies — the shim sends messages, and the shell handles identity, signing, and encryption. No window.nostr is installed.
- npm:
@napplet/shim - JSR:
@napplet/shim - Source: packages/shim
Maturity-controlled injection
Use the side-effect-free @napplet/shim/prelude entrypoint for policy-controlled hosts. The legacy root @napplet/shim import still installs every bundled domain for compatibility.
import { selectNapDomains } from '@napplet/core';
import { renderNappletRuntimePreludeCall } from '@napplet/shim/prelude';
const selection = selectNapDomains({
candidates: ['storage', 'llm', 'ble'],
optIn: ['llm'],
});
const activate = renderNappletRuntimePreludeCall({ domains: selection.domains });Inline the maintained prelude artifact followed by activate before napplet scripts run. Enforce the same selection.domains set in host dispatch: hiding a shim object does not block direct postMessage. Candidates must already be implemented and authorized for the napplet.
Prelude helpers also accept maturity: {} for supported-only filtering or maturity: { optIn: ['llm'] } for a named exception. Omitting maturity preserves existing explicit allowlist behavior. An explicit install replaces an earlier legacy namespace but cannot undo earlier calls; finish host setup before napplet code executes. See the maturity guide for classifications, required/optional domain handling, and provenance limits.
Install
npm install @napplet/shimRuntime export
installNappletGlobal installs selected domain objects onto a target window. Napplet-side code should use @napplet/sdk or direct typed window.napplet access.
For iframe.srcdoc runtimes, @napplet/shim/prelude exposes a host-injectable surface that does not require every napplet bundle to import the shim. Inline the npm browser artifact from @napplet/shim/prelude.global, then activate it with an explicit domain allowlist:
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { renderNappletRuntimePreludeCall } from '@napplet/shim/prelude';
const require = createRequire(import.meta.url); const preludeSource = readFileSync( require.resolve('@napplet/shim/prelude.global'), 'utf8', );
const activatePrelude = renderNappletRuntimePreludeCall({ domains: ['identity', 'storage', 'outbox'], });
const srcdoc = html.replace(
'<head>',
`<head><script>${preludeSource}\n${activatePrelude}</script>`,
);The IIFE artifact exposes globalThis.NappletShimPrelude.install({ domains }) and installs only the requested known NAP domains. JSR exposes the source ESM helpers under @napplet/shim/prelude; the generated prelude.global artifact is npm-only.
The window.napplet shape
After runtime injection, the global may be populated with these sub-objects:
| Namespace | What it does |
|---|---|
outbox | Outbox-aware getEvent, query, subscribe, publish, and resolveRelays; default for normal event reads and publishes |
common | Profile lookup, follow/unfollow, reactions, reports, and NIP-19 helpers |
lists | NIP-51 list read and mutation helpers |
count | Count queries through the shell |
dm | Shell-mediated encrypted direct-message helpers |
relay | Low-level explicit relay proxy for relay-local escape hatches |
inc | Inter-napplet communication: emit, on |
intent | Archetype-based intent invocation, discovery, and availability changes |
storage | Scoped key-value storage: getItem, setItem, removeItem, keys (512 KB quota), plus storage.instance.* for per-instance scope |
keys | Keyboard forwarding + action keybindings: registerAction, unregisterAction, onAction |
media | Ownership-aware media sessions: createSession, reportState, onCommand, … |
notify | Shell-rendered notifications: send, badge, onAction, … |
identity | Read-only user queries: getPublicKey, onChanged, getProfile, … |
config | Per-napplet declarative config: get, subscribe, openSettings, registerSchema, schema |
resource | Sandboxed byte fetching: info, bytes, bytesMany, bytesAsObjectURL |
| Domain absence | If a property is absent, that NAP is unavailable to the napplet. |
Usage
import { installNappletGlobal } from '@napplet/shim';
installNappletGlobal({
domains: ['outbox', 'storage', 'identity', 'inc', 'intent'],
});Napplet application code then consumes injected domains:
// Read kind 1 notes through outbox-aware routing
const { events } = await window.napplet.outbox.query(
[{ kinds: [1], limit: 20 }],
{ timeoutMs: 3000 },
);
for (const result of events) console.log('Note:', result.event.content);
// Subscribe to live updates through the same outbox boundary const sub = window.napplet.outbox.subscribe([{ kinds: [1], limit: 20 }], { timeoutMs: 3000, }); sub.on('event', (result) => console.log('New note:', result.event.content));
// Publish a note (the shell signs and fans it out) const published = await window.napplet.outbox.publish({ kind: 1, content: 'Hello from my napplet!', tags: [], created_at: Math.floor(Date.now() / 1000), }); if (!published.ok) throw new Error(published.error ?? 'publish failed');
// NAP-INC convention URI emission: the runtime sends the stable topic with // a shallow text payload. `pubkey` is a local convention choice. window.napplet.inc.emit('napplet:profile/open?pubkey=abc123'); const profileOpen = window.napplet.inc.on('napplet:profile/open', (event) => { validateProfileOpenPayload(event.payload); });
const intentResult = await window.napplet.intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } });
// Scoped storage, proxied through the shell await window.napplet.storage.setItem('theme', 'dark'); const theme = await window.napplet.storage.getItem('theme'); // 'dark'
// Read-only identity const pubkey = await window.napplet.identity.getPublicKey(); // "" when signed out
// Feature-gate before using a domain if (window.napplet?.media) { const { sessionId } = await window.napplet.media.createSession({ owner: 'napplet', metadata: { title: 'My Song', artist: 'The Artist' }, }); }
sub.close();
profileOpen.close();INC convention URIs
The NAP-INC shim accepts a queried napplet:<archetype>/<intent> URI only at emit(topic, payload?). It transposes the query before posting inc.emit, so the shell and consumers see the exact queryless stable topic and a shallow decoded text payload. Subscriptions and routing never parse, normalize, prefix-match, or wildcard-match topics after that boundary.
Fragments, malformed percent escapes, repeated decoded names, and a queried URI with an explicit payload reject before emission. Use a queryless topic and its explicit payload for structured data.
Intent dispatch injection
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 behavior defers to the living NAP-INC and NAP-INTENT documents.
TypeScript support
The shim does not modify global Window types in its published source (so it is accepted by JSR). For typed window.napplet access, cast using NappletGlobal from @napplet/core, or — preferably — use the named helpers in @napplet/sdk:
import type { NappletGlobal } from '@napplet/core';
const napplet = (window as Window & { napplet: NappletGlobal }).napplet;Wire format
The shim communicates with the shell using JSON envelope messages ({ type: 'domain.action', ...payload }). installNappletGlobal first opens the NIP-5D channel: it creates a MessageChannel, keeps one port private and hands the other to the shell once in napplet.hello. Outbound and inbound envelopes then travel only over that port; window-level messages are ignored. Request/response pairs are correlated by an id field. The shim package README documents the full per-domain outbound and inbound message catalog.
See also
- Shim vs. SDK
@napplet/sdk— named, typed wrapper over the same global
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 runtime binding
Select llm in installNappletGlobal({ domains: ['llm'] }) or the equivalent runtime prelude options to expose window.napplet.llm. The binding forwards select, describe, generate, and cancel, correlates each result by id and type, routes llm.delta only to the generation that owns its requestId, drops deltas that do not match the Delta schema (start / delta / end events, text only on delta, toolCallId and name only on a toolCall start), stops provisional delivery on a sequence gap, rejects an Error record as an error carrying the specification code, and maps an abort signal to llm.cancel. Installing the binding supplies no connection, consent, or provider: the host provides policy-bound handlers, endpoint-scoped handles, and prompts. See the living NAP-LLM proposal.