npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@napplet/shim

v0.3.1

Published

Napplet SDK — subscribe, publish, query, emit, on, nappStorage for Nostr-native iframe applications

Readme

@napplet/shim

Side-effect-only window installer for napplet iframes. Importing @napplet/shim installs the window.napplet global. 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

  1. Import @napplet/shim in your napplet's entry point (side-effect only -- no named exports)
  2. The shim registers with the shell via postMessage -- the shell assigns identity based on the iframe's message.source Window reference
  3. Once registered, window.napplet is populated with relay, ifc, storage, keys, media, notify, identity, config, resource, connect, class, and shell sub-objects
  4. No window.nostr is installed -- signing and encryption are mediated by the shell via relay.publish() and relay.publishEncrypted()

Installation

npm install @napplet/shim

Quick 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-frame events from other napplets
const ifcSub = window.napplet.ifc.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' });

// Fetch external bytes via the shell (CSP blocks direct <img src=externalUrl> / fetch())
const avatarBlob = await window.napplet.resource.bytes('https://example.com/avatar.png');
const handle = window.napplet.resource.bytesAsObjectURL('blossom:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
imgEl.src = handle.url;
// later: handle.revoke();

// Check the shell-assigned class (undefined if shell doesn't implement nub:class)
if (window.napplet.shell.supports('nub:class')) {
  const cls = window.napplet.class;
  if (cls === 2) { /* user-approved explicit-origin posture */ }
}

// Use direct network access if the user approved `connect` origins at build time
if (window.napplet.connect.granted) {
  const res = await fetch(`${window.napplet.connect.origins[0]}/items`);
  const data = await res.json();
}

// Clean up
sub.close();
ifcSub.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 }

{ type: 'resource.bytes', id: string, url: string }
{ type: 'resource.cancel', id: string }

// (NUB-CONNECT has no postMessage wire — grants flow via CSP header + <meta name="napplet-connect-granted">)

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 }

{ type: 'resource.bytes.result', id: string, blob: Blob, mime: string }
{ type: 'resource.bytes.error', id: string, error: 'not-found' | 'blocked-by-policy' | 'timeout' | 'too-large' | 'unsupported-scheme' | 'decode-failed' | 'network-error' | 'quota-exceeded', message?: string }

{ type: 'class.assigned', id: string, class: number }

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[]>;
  },
  ifc: {
    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;
  },
  resource: {
    bytes(url, opts?): Promise<Blob>;
    bytesAsObjectURL(url): { url: string; revoke: () => void };
  },
  connect: {
    readonly granted: boolean;
    readonly origins: readonly string[];
  },
  class?: number,   // shell-assigned via class.assigned envelope; undefined on shells without nub:class
  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.ifc

Inter-frame 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, no keys.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.resource

Sandboxed byte fetching. The iframe sandbox (no allow-same-origin) plus strict CSP (no connect-src) means napplets cannot fetch external URLs directly — <img src="https://...">, fetch(), and XMLHttpRequest are all blocked by the browser. Use resource.bytes(url) to fetch any external resource through the shell.

| Method | Returns | Description | |--------|---------|-------------| | bytes(url, opts?) | Promise<Blob> | Fetch bytes for a URL via the shell. opts.signal accepts an AbortSignal for cancellation. | | bytesAsObjectURL(url) | { url: string; revoke: () => void } | Synchronous handle whose url resolves to a blob URL once the underlying fetch completes. Caller MUST revoke() when done. |

Four canonical schemes: data: (decoded in-shim), https: (shell-side network with policy), blossom:sha256:<hex> (hash-verified), nostr:<bech32> (single-hop NIP-19 resolution).

Errors reject the Promise with one of 8 codes: not-found, blocked-by-policy, timeout, too-large, unsupported-scheme, decode-failed, network-error, quota-exceeded.

Capability detection:

if (window.napplet.shell.supports('nub:resource')) { /* ... */ }
if (window.napplet.shell.supports('resource:scheme:blossom')) { /* ... */ }
if (window.napplet.shell.supports('perm:strict-csp')) { /* shell enforces strict CSP */ }

window.napplet.connect

User-gated direct network access (NUB-CONNECT). NO postMessage wire — the shim reads <meta name="napplet-connect-granted" content="<space-separated-origins>"> synchronously at install time. Napplets declare required origins at build time via @napplet/vite-plugin's connect: string[] option; the user is prompted by the shell at first load per (dTag, aggregateHash); on approval the shell emits a runtime CSP whose connect-src contains the approved origins AND injects the discovery meta tag.

| Field | Type | Description | |-------|------|-------------| | granted | boolean | true when the user approved all declared origins for this (dTag, aggregateHash). false on denial, on shells without nub:connect, or pre-injection. | | origins | readonly string[] | The user-approved origins (already normalized per the shared normalizeConnectOrigin validator). Empty on denial. |

Graceful-degradation default: window.napplet.connect === { granted: false, origins: [] } on shells that do not advertise nub:connect or have not injected the meta tag. The property is NEVER undefined.

if (window.napplet.shell.supports('nub:connect') && window.napplet.connect.granted) {
  // Direct fetch / WebSocket to window.napplet.connect.origins is permitted.
} else {
  // Fall back to window.napplet.resource.bytes(url) for read-only byte fetches.
}

Capability detection (operator-policy refinements):

if (window.napplet.shell.supports('connect:scheme:http')) { /* cleartext http: origins permitted */ }
if (window.napplet.shell.supports('connect:scheme:ws'))   { /* cleartext ws: origins permitted */ }

window.napplet.class

Shell-assigned integer class (NUB-CLASS). The shell sends exactly one class.assigned envelope per napplet lifecycle at iframe-ready time; the shim writes the integer to window.napplet.class via a defineProperty getter.

| Field | Type | Description | |-------|------|-------------| | class | number \| undefined | The class integer from the class.assigned envelope. undefined until the envelope arrives, or permanently undefined on shells that do not implement nub:class. |

Graceful-degradation default: window.napplet.class === undefined on shells without nub:class, or before the wire envelope arrives. Never 0, never null. Napplets SHOULD check shell.supports('nub:class') before branching on the value to distinguish "shell doesn't implement" from "envelope hasn't arrived yet".

v0.29.0 ships two track members:

  • class: 1 → NUB-CLASS-1 (strict baseline; connect-src 'none')
  • class: 2 → NUB-CLASS-2 (user-approved explicit-origin; connect-src <granted-origins>)

The class integer is informational to the napplet; the shell enforces the posture via the CSP it serves with the HTML. Napplet code MUST NOT attempt to infer its own class from observed CSP or other signals — only class.assigned is authoritative.

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 installs window.napplet at runtime. The package does not modify global Window types in its published source so it can be accepted by JSR. For direct window.napplet access, use NappletGlobal from @napplet/core in a local cast or ambient declaration:

import type { NappletGlobal } from '@napplet/core';
import '@napplet/shim';

const napplet = (window as Window & { napplet: NappletGlobal }).napplet;

napplet.relay.subscribe({ kinds: [1] }, (event) => {
  // event is typed as NostrEvent
});

napplet.shell.supports('identity'); // typed as (capability: string) => boolean

For named typed helpers, prefer @napplet/sdk; it wraps window.napplet without requiring global type augmentation.

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, ifc } from '@napplet/sdk' | | What it does | Installs window.napplet global + shell registration | Named exports wrapping window.napplet | | Dependencies | @napplet/nub (uses @napplet/nub/<domain>/shim subpaths internally) | @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, ifc, storage, keys, identity, plus types |

Typical usage: Import both -- shim for window installation, SDK for typed API access:

import '@napplet/shim';
import { relay, ifc, storage, keys, identity } from '@napplet/sdk';

Protocol Reference

  • NIP-5D -- Napplet-shell protocol specification

License

MIT