Package Exports
- @napplet/shim
Readme
@napplet/shim
Side-effect-only window installer for napplet iframes. Importing
@napplet/shiminstalls thewindow.nappletglobal. No named exports. No cryptographic dependencies -- the shim sends JSON envelope messages and the shell handles identity.
Getting Started
Prerequisites
- A shell host running a napplet protocol shell implementation
How It Works
- Import
@napplet/shimin your napplet's entry point (side-effect only -- no named exports) - The shim registers with the shell via postMessage -- the shell assigns identity based on the iframe's
message.sourceWindow reference - Once registered,
window.nappletis populated with relay, ipc, storage, keys, media, notify, identity, config, and shell sub-objects - No
window.nostris installed -- signing and encryption are mediated by the shell viarelay.publish()andrelay.publishEncrypted()
Installation
npm install @napplet/shimQuick Start
// Side-effect import -- installs window.napplet (no window.nostr)
import '@napplet/shim';
// Subscribe to kind 1 notes
const sub = window.napplet.relay.subscribe(
{ kinds: [1], limit: 20 },
(event) => console.log('New note:', event.content),
() => console.log('End of stored events'),
);
// Publish a note (shell signs it)
const signed = await window.napplet.relay.publish({
kind: 1,
content: 'Hello from my napplet!',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
// Listen for inter-pane events from other napplets
const ipcSub = window.napplet.ipc.on('profile:open', (payload) => {
console.log('Profile requested:', payload);
});
// Use scoped storage (proxied through the shell)
await window.napplet.storage.setItem('theme', 'dark');
const theme = await window.napplet.storage.getItem('theme'); // 'dark'
// Register a keyboard action the shell can bind to a key
const result = await window.napplet.keys.registerAction({
id: 'editor.save', label: 'Save', defaultKey: 'Ctrl+S',
});
// Listen for the bound key locally (zero-latency, no postMessage round-trip)
const keySub = window.napplet.keys.onAction('editor.save', () => {
console.log('Save triggered!');
});
// Create a media session
const { sessionId } = await window.napplet.media.createSession({
title: 'My Song', artist: 'The Artist',
});
// Report playback state
window.napplet.media.reportState(sessionId, {
status: 'playing', position: 42.5, duration: 240,
});
// Listen for shell media commands
const mediaSub = window.napplet.media.onCommand(sessionId, (action, value) => {
if (action === 'pause') player.pause();
});
// Send a notification
const { notificationId } = await window.napplet.notify.send({
title: 'New message', body: 'Alice: hey!', priority: 'normal',
});
// Set badge count
window.napplet.notify.badge(3);
// Listen for notification interactions
const notifySub = window.napplet.notify.onAction((notifId, actionId) => {
if (actionId === 'reply') openReply(notifId);
});
// Get user identity (read-only)
const pubkey = await window.napplet.identity.getPublicKey();
const profile = await window.napplet.identity.getProfile();
// Read per-napplet config (validated + defaulted by the shell)
const config = await window.napplet.config.get();
// Subscribe to live config updates
const configSub = window.napplet.config.subscribe((values) => {
applyTheme(values.theme);
});
// Deep-link the shell's settings UI to a named section
window.napplet.config.openSettings({ section: 'appearance' });
// Clean up
sub.close();
ipcSub.close();
keySub.close();
mediaSub.close();
notifySub.close();
configSub.close();Wire Format
The shim communicates with the shell using JSON envelope messages ({ type: "domain.action", ...payload }) as defined by NIP-5D.
Outbound (napplet → shell)
Messages sent via window.parent.postMessage(msg, '*'):
{ type: 'relay.subscribe', id: string, subId: string, filters: NostrFilter[] }
{ type: 'relay.publish', id: string, event: EventTemplate }
{ type: 'relay.publishEncrypted', id: string, event: EventTemplate, recipient: string, encryption?: 'nip44' | 'nip04' }
{ type: 'relay.query', id: string, filters: NostrFilter[] }
{ type: 'relay.unsubscribe', subId: string }
{ type: 'identity.getPublicKey', id: string }
{ type: 'identity.getRelays', id: string }
{ type: 'identity.getProfile', id: string }
{ type: 'identity.getFollows', id: string }
{ type: 'identity.getList', id: string, listType: string }
{ type: 'identity.getZaps', id: string }
{ type: 'identity.getMutes', id: string }
{ type: 'identity.getBlocked', id: string }
{ type: 'identity.getBadges', id: string }
{ type: 'ifc.emit', topic: string, payload?: unknown }
{ type: 'ifc.subscribe', id: string, topic: string }
{ type: 'ifc.unsubscribe', topic: string }
{ type: 'storage.get', id: string, key: string }
{ type: 'storage.set', id: string, key: string, value: string }
{ type: 'storage.remove', id: string, key: string }
{ type: 'storage.keys', id: string }
{ type: 'keys.forward', key: string, code: string, ctrl: boolean, alt: boolean, shift: boolean, meta: boolean }
{ type: 'keys.registerAction', id: string, action: { id: string, label: string, defaultKey?: string } }
{ type: 'keys.unregisterAction', actionId: string }
{ type: 'media.session.create', id: string, sessionId: string, metadata?: object }
{ type: 'media.session.update', sessionId: string, metadata: object }
{ type: 'media.session.destroy', sessionId: string }
{ type: 'media.state', sessionId: string, status: string, position?: number, duration?: number, volume?: number }
{ type: 'media.capabilities', sessionId: string, actions: string[] }
{ type: 'notify.send', id: string, title: string, body?: string, icon?: string, actions?: object[], channel?: string, priority?: string }
{ type: 'notify.dismiss', notificationId: string }
{ type: 'notify.badge', count: number }
{ type: 'notify.channel.register', channelId: string, label: string, description?: string, defaultPriority?: string }
{ type: 'notify.permission.request', id: string, channel?: string }
{ type: 'config.registerSchema', id: string, schema: object, version?: number }
{ type: 'config.get', id: string }
{ type: 'config.subscribe' }
{ type: 'config.unsubscribe' }
{ type: 'config.openSettings', section?: string }Inbound (shell → napplet)
Messages received via window.addEventListener('message', ...):
{ type: 'relay.event', subId: string, event: NostrEvent }
{ type: 'relay.eose', subId: string }
{ type: 'relay.publish.result', id: string, ok: boolean, event?: NostrEvent, error?: string }
{ type: 'relay.publishEncrypted.result', id: string, ok: boolean, event?: NostrEvent, error?: string }
{ type: 'relay.query.result', id: string, events: NostrEvent[], error?: string }
{ type: 'identity.getPublicKey.result', id: string, pubkey: string }
{ type: 'identity.getRelays.result', id: string, relays: Record<string, { read: boolean, write: boolean }>, error?: string }
{ type: 'identity.getProfile.result', id: string, profile: object | null, error?: string }
{ type: 'identity.getFollows.result', id: string, pubkeys: string[], error?: string }
{ type: 'identity.getList.result', id: string, entries: string[], error?: string }
{ type: 'identity.getZaps.result', id: string, zaps: object[], error?: string }
{ type: 'identity.getMutes.result', id: string, pubkeys: string[], error?: string }
{ type: 'identity.getBlocked.result', id: string, pubkeys: string[], error?: string }
{ type: 'identity.getBadges.result', id: string, badges: object[], error?: string }
{ type: 'ifc.event', topic: string, payload?: unknown, sender: string }
{ type: 'storage.get.result', id: string, value?: string | null, error?: string }
{ type: 'storage.set.result', id: string, error?: string }
{ type: 'storage.remove.result', id: string, error?: string }
{ type: 'storage.keys.result', id: string, keys?: string[], error?: string }
{ type: 'keys.registerAction.result', id: string, actionId: string, binding?: string, error?: string }
{ type: 'keys.bindings', bindings: Array<{ actionId: string, key: string }> }
{ type: 'keys.action', actionId: string }
{ type: 'media.session.create.result', id: string, sessionId: string, error?: string }
{ type: 'media.command', sessionId: string, action: string, value?: number }
{ type: 'media.controls', controls: string[] }
{ type: 'notify.send.result', id: string, notificationId?: string, error?: string }
{ type: 'notify.permission.result', id: string, granted: boolean }
{ type: 'notify.action', notificationId: string, actionId: string }
{ type: 'notify.clicked', notificationId: string }
{ type: 'notify.dismissed', notificationId: string, reason?: string }
{ type: 'notify.controls', controls: string[] }
{ type: 'config.registerSchema.result', id: string, ok: boolean, code?: string, error?: string }
{ type: 'config.values', id?: string, values: object }
{ type: 'config.schemaError', code: string, error: string }All request/response pairs are correlated by the id field. Identity request timeouts after 30 seconds.
window.napplet Shape
After import '@napplet/shim', the global window.napplet object has the following structure:
window.napplet = {
relay: {
subscribe(filters, onEvent, onEose, options?): Subscription;
publish(template, options?): Promise<NostrEvent>;
publishEncrypted(template, recipient, encryption?): Promise<NostrEvent>;
query(filters): Promise<NostrEvent[]>;
},
ipc: {
emit(topic, extraTags?, content?): void;
on(topic, callback): { close(): void };
},
storage: {
getItem(key): Promise<string | null>;
setItem(key, value): Promise<void>;
removeItem(key): Promise<void>;
keys(): Promise<string[]>;
},
keys: {
registerAction(action): Promise<{ actionId: string; binding?: string }>;
unregisterAction(actionId): void;
onAction(actionId, callback): { close(): void };
},
media: {
createSession(metadata?): Promise<{ sessionId: string }>;
updateSession(sessionId, metadata): void;
destroySession(sessionId): void;
reportState(sessionId, state): void;
reportCapabilities(sessionId, actions): void;
onCommand(sessionId, callback): { close(): void };
onControls(sessionId, callback): { close(): void };
},
notify: {
send(notification): Promise<{ notificationId: string }>;
dismiss(notificationId): void;
badge(count): void;
registerChannel(channel): void;
requestPermission(channel?): Promise<{ granted: boolean }>;
onAction(callback): { close(): void };
onClicked(callback): { close(): void };
onDismissed(callback): { close(): void };
onControls(callback): { close(): void };
},
identity: {
getPublicKey(): Promise<string>;
getRelays(): Promise<Record<string, { read: boolean; write: boolean }>>;
getProfile(): Promise<object | null>;
getFollows(): Promise<string[]>;
getList(listType): Promise<string[]>;
getZaps(): Promise<object[]>;
getMutes(): Promise<string[]>;
getBlocked(): Promise<string[]>;
getBadges(): Promise<object[]>;
},
config: {
registerSchema(schema, version?): Promise<void>;
get(): Promise<Record<string, unknown>>;
subscribe(callback): { close(): void };
openSettings(options?): void;
onSchemaError(callback): () => void;
readonly schema: Record<string, unknown> | null;
},
shell: {
supports(capability: NamespacedCapability): boolean;
},
};window.napplet.relay
Relay operations through the shell's relay pool via JSON envelope (relay.subscribe, relay.publish, relay.query messages).
| Method | Returns | Description |
|---|---|---|
subscribe(filters, onEvent, onEose, options?) |
Subscription |
Open a relay subscription via JSON envelope. options.relay and options.group for NIP-29 scoped relays. |
publish(template, options?) |
Promise<NostrEvent> |
Send an event template to the shell for signing and broadcast. |
publishEncrypted(template, recipient, encryption?) |
Promise<NostrEvent> |
Send an event template to the shell for encryption, signing, and broadcast. NIP-44 default. |
query(filters) |
Promise<NostrEvent[]> |
One-shot query: sends a relay.query envelope, resolves when results arrive. |
window.napplet.ipc
Inter-pane communication between napplets via the shell.
| Method | Returns | Description |
|---|---|---|
emit(topic, extraTags?, content?) |
void |
Send an ifc.emit JSON envelope to the shell for delivery to matching topic subscribers. |
on(topic, callback) |
{ close(): void } |
Subscribe to ifc.event JSON envelopes on a topic. Callback receives (payload, event). |
window.napplet.storage
Sandboxed key-value storage proxied through the shell. Scoped by napplet identity -- napplets cannot read each other's data. 512 KB quota per napplet.
| Method | Returns | Description |
|---|---|---|
getItem(key) |
Promise<string | null> |
Retrieve a stored value. Returns null if key does not exist. |
setItem(key, value) |
Promise<void> |
Store a key-value pair. Throws on quota exceeded. |
removeItem(key) |
Promise<void> |
Remove a stored key. |
keys() |
Promise<string[]> |
List all keys stored by this napplet. |
window.napplet.keys
Keyboard forwarding and action keybindings. The shim installs a capture-phase keydown listener that implements smart forwarding: unbound keys are forwarded to the shell via keys.forward, while bound keys are handled locally with zero latency.
| Method | Returns | Description |
|---|---|---|
registerAction(action) |
Promise<{ actionId, binding? }> |
Declare a named action the shell can bind to a key. defaultKey is a hint. |
unregisterAction(actionId) |
void |
Remove a previously registered action. Fire-and-forget. |
onAction(actionId, callback) |
{ close(): void } |
Register a local handler for a bound key. NOT a wire message -- zero latency. |
Smart forwarding rules:
- Text inputs (
<input>,<textarea>,contenteditable) are never forwarded (prevents credential leakage) - Bare modifier keys are never forwarded
- IME composition events are never forwarded
- Reserved keys (
Tab,Shift+Tab,Escape) are never suppressed - Bound keys:
preventDefault()+ local action handler, nokeys.forward - Unbound keys: forwarded to shell via
keys.forward
window.napplet.media
Media session control. Create sessions, report playback state and metadata, declare capabilities, and receive commands from the shell.
| Method | Returns | Description |
|---|---|---|
createSession(metadata?) |
Promise<{ sessionId }> |
Create a new media session with optional metadata. |
updateSession(sessionId, metadata) |
void |
Update metadata for an existing session. Fire-and-forget. |
destroySession(sessionId) |
void |
Destroy a session. Fire-and-forget. |
reportState(sessionId, state) |
void |
Report playback state (status, position, duration, volume). |
reportCapabilities(sessionId, actions) |
void |
Declare supported media actions (dynamic). |
onCommand(sessionId, callback) |
{ close(): void } |
Listen for shell media commands (play, pause, seek, volume, etc.). |
onControls(sessionId, callback) |
{ close(): void } |
Listen for the shell's supported control list. |
window.napplet.notify
Shell-rendered notifications. Send notifications, set badge counts, register channels, request permission, and listen for user interaction.
| Method | Returns | Description |
|---|---|---|
send(notification) |
Promise<{ notificationId }> |
Send a notification to the shell. |
dismiss(notificationId) |
void |
Dismiss a notification. Fire-and-forget. |
badge(count) |
void |
Set badge count (0 to clear). Fire-and-forget. |
registerChannel(channel) |
void |
Register a notification channel. Fire-and-forget. |
requestPermission(channel?) |
Promise<{ granted }> |
Request permission to send notifications. |
onAction(callback) |
{ close(): void } |
Listen for action button clicks. |
onClicked(callback) |
{ close(): void } |
Listen for notification body clicks. |
onDismissed(callback) |
{ close(): void } |
Listen for dismissals (user/timeout/replaced). |
onControls(callback) |
{ close(): void } |
Listen for shell's notification capabilities. |
window.napplet.config
Per-napplet declarative configuration (NUB-CONFIG). The shell is the sole writer; napplets subscribe to live values, request snapshots, register runtime schemas, and deep-link the shell's settings UI.
| Method | Returns | Description |
|---|---|---|
registerSchema(schema, version?) |
Promise<void> |
Register a schema at runtime (escape hatch -- prefer manifest-driven via @napplet/vite-plugin). |
get() |
Promise<Record<string, unknown>> |
One-shot snapshot of validated + defaulted values. |
subscribe(callback) |
{ close(): void } |
Live push stream; wire-level subscribe emitted on 0->1 local-subscriber transition. |
openSettings(options?) |
void |
Ask the shell to open its settings UI, optionally deep-linked to an x-napplet-section name. |
onSchemaError(callback) |
() => void |
Listen for uncorrelated config.schemaError pushes (returns a plain teardown fn). |
schema (accessor) |
Record<string, unknown> | null |
Readonly current schema snapshot (manifest-declared or last-accepted runtime registration). |
window.napplet.shell
Namespaced capability query. supports() checks whether the shell declared support for a NUB domain or permission.
// NUB domains (bare shorthand or nub: prefix)
window.napplet.shell.supports('relay'); // bare shorthand
window.napplet.shell.supports('nub:identity'); // explicit prefix
// Permissions
window.napplet.shell.supports('perm:popups');Currently returns false until the shell populates it at iframe creation time. Use as a feature gate before calling APIs that depend on a specific capability.
TypeScript Support
Importing @napplet/shim activates a global Window type augmentation:
// This side-effect import gives TypeScript full autocompletion for window.napplet.*
import '@napplet/shim';
// TypeScript knows about window.napplet.relay, .ipc, .storage, .keys, .media, .notify, .identity, .shell
window.napplet.relay.subscribe({ kinds: [1] }, (event) => {
// event is typed as NostrEvent
});
window.napplet.shell.supports('identity'); // typed as (capability: string) => booleanThe NappletGlobal interface is defined in @napplet/core and augmented onto Window by the shim's type declarations.
Note: @napplet/shim has zero named exports -- import { anything } from '@napplet/shim' is a TypeScript error. For named imports, use @napplet/sdk.
Shim vs SDK
@napplet/shim |
@napplet/sdk |
|
|---|---|---|
| Import style | import '@napplet/shim' (side-effect) |
import { relay, ipc } from '@napplet/sdk' |
| What it does | Installs window.napplet global + shell registration |
Named exports wrapping window.napplet |
| Dependencies | @napplet/nub-relay, @napplet/nub-identity, @napplet/nub-ifc, @napplet/nub-keys, @napplet/nub-media, @napplet/nub-notify, @napplet/nub-config |
@napplet/core (types only) |
| When to use | Always -- required to install the runtime | When you want typed imports in a bundler |
| Named exports | None | relay, ipc, storage, keys, identity, plus types |
Typical usage: Import both -- shim for window installation, SDK for typed API access:
import '@napplet/shim';
import { relay, ipc, storage, keys, identity } from '@napplet/sdk';Protocol Reference
- NIP-5D -- Napplet-shell protocol specification
License
MIT