@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
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(
'profile',
{ pubkey: 'abc123' },
{ convention: 'napplet:profile/open', behavior: { newWindow: true } },
);
// 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
The intent shim exposes invoke(request) and open(archetype, payload?, opts?). Results contain required ok, archetype, action, and handled fields plus optional handler, window, convention, and error details.
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 }). Outbound messages go via window.parent.postMessage(msg, '*'); inbound arrive via a message listener. 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