@napplet/sdk
v0.27.2
Published
Typed named exports wrapping window.napplet for bundler-consuming napplet developers
Maintainers
Readme
@napplet/sdk
Named TypeScript exports for napplet developers using a bundler. Wraps
window.nappletat call time.
Getting Started
Prerequisites
- A NIP-5D runtime injects
window.nappletbefore SDK methods are called - A shell host running a napplet protocol shell implementation
How It Works
- Import named exports from
@napplet/sdk--outbox,common,relay,inc,storage,keys,ble,count,lists - Each SDK method delegates to its injected
window.napplet.*counterpart at call time - If
window.nappletor a requested domain is unavailable when a method is called, a descriptive error is thrown
Installation
npm install @napplet/sdkQuick Start
import { outbox, common, inc, storage, keys, media, notify, config, resource, ble, webrtc, link, count, lists } from '@napplet/sdk';
// Read kind 1 notes through outbox-aware routing
const { events } = await 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 = outbox.subscribe([{ kinds: [1], limit: 20 }], { timeoutMs: 3000 });
sub.on('event', (result) => console.log('New note:', result.event.content));
// Publish a signed note through the user's outbox/write relays
const published = await outbox.publish({
kind: 1,
content: 'Hello from my napplet!',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
if (!published.ok || !published.event) throw new Error(published.error ?? 'publish failed');
// Common social actions keep consent, event construction, signing, and relay routing in the shell
await common.react(published.event.id, '+');
// Inter-napplet messaging. The payload is this application's local choice.
inc.emit('napplet:note/open', { targetId: 'local-note-id' });
const incSub = inc.on('napplet:note/open', (event) => {
console.log('Local note-open payload:', event.payload);
});
// Scoped storage
await storage.setItem('theme', 'dark');
const theme = await storage.getItem('theme'); // 'dark'
// Register keyboard action
const result = await keys.registerAction({
id: 'editor.save', label: 'Save', defaultKey: 'Ctrl+S',
});
// Listen for bound key locally
const keySub = keys.onAction('editor.save', () => {
console.log('Save triggered!');
});
// Create a media session
const { sessionId } = await media.createSession({
owner: 'napplet',
metadata: { title: 'My Song', artist: 'The Artist' },
});
media.reportState(sessionId, { status: 'playing', position: 42.5, duration: 240 });
// Send a notification
const { notificationId } = await notify.send({
title: 'Task complete', body: 'Build succeeded', priority: 'normal',
});
notify.badge(1);
// Read per-napplet config (shell-validated + defaulted)
const values = await config.get();
console.log('Current theme:', values.theme);
// Subscribe to live config updates
const configSub = config.subscribe((v) => {
applyTheme(v.theme);
});
// Deep-link settings UI
config.openSettings({ section: 'appearance' });
// Fetch external bytes via the shell (the iframe sandbox + strict CSP block direct fetch)
const avatarBlob = await resource.bytes('https://example.com/avatar.png');
const resourceItems = await resource.bytesMany([
'https://example.com/avatar.png',
'blossom:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
]);
const handle = resource.bytesAsObjectURL('blossom:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
imgEl.src = handle.url;
// handle.revoke() when done
// Open a shell-mediated WebRTC data session
const { session } = await webrtc.open({ scope: { type: 'direct', pubkey: 'abc123...' } });
await webrtc.send(session.id, { body: 'hello' });
// Open a shell-mediated BLE session and inspect exposed services
const { session: bleSession } = await ble.open({ acceptAllDevices: true });
const bleServices = await ble.services(bleSession.id);
// Open an external URL through the shell
await link.open('https://example.com/post/123', { label: 'Read post' });
// Add an item to a supported NIP-51 list through the runtime
await lists.add({ type: 'mute-list' }, [{ itemType: 'pubkey', value: 'abc123...' }]);
// Clean up
sub.close();
incSub.close();
keySub.close();
configSub.close();API Reference
relay
Low-level relay operations through the shell's relay pool. Mirrors window.napplet.relay. Use this for explicit relay-local behavior such as group relays, diagnostics, and protocol tooling. For normal social reads/publishes, prefer outbox or a higher-level domain such as common, lists, count, or dm.
| Method | Returns | Description |
|--------|---------|-------------|
| subscribe(filters, onEvent, onEose, options?) | Subscription | Open a live relay subscription through the shell's relay pool; onEvent receives RelayEventResult |
| publish(template, options?) | Promise<NostrEvent> | Send event template to the shell for signing and broadcast |
| publishEncrypted(template, recipient, encryption?) | Promise<NostrEvent> | Send event template for encryption, signing, and broadcast |
| query(filters) | Promise<RelayEventResult[]> | One-shot query: collect RelayEventResult records until EOSE, resolve |
outbox
Outbox-aware relay routing. Mirrors window.napplet.outbox. The napplet supplies filters, event ids, templates, and intent; the shell owns NIP-65 relay discovery, fallback, deduplication, signature validation, signing, and publish fanout.
| Method | Returns | Description |
|--------|---------|-------------|
| getEvent(eventId, options?) | Promise<OutboxEventResult> | Fetch one event through author/relay-aware routing |
| query(filters, options?) | Promise<OutboxResult> | One-shot outbox-aware query returning RelayEventResult[] |
| subscribe(filters, options?) | OutboxSubscription | Live outbox-aware stream with on('event') and close() |
| publish(template, options?) | Promise<OutboxPublishResult> | Shell-sign and fan out through the user's write relays and directed inbox relays |
| resolveRelays(target) | Promise<OutboxRelayPlan> | Diagnostic/advisory relay plan; prefer query/subscribe/publish for app behavior |
common
Common social actions. Mirrors window.napplet.common. Use it for NIP-19 helpers, profile lookup, follows, follow/unfollow, reactions, and reports so the shell owns consent, event construction, signing, publishing, and lookup policy.
inc
Inter-napplet communication between napplets. Mirrors window.napplet.inc.
Messages are sent as JSON envelope objects ({ type: 'inc.emit', topic, payload }) and received as ({ type: 'inc.event', topic, payload, sender }). Topics are opaque strings: a sender and subscriber use the same complete value. The package does not prescribe convention payload schemas, wildcard, prefix, or canonicalization behavior.
| Method | Returns | Description |
|--------|---------|-------------|
| emit(topic, payload?) | void | Broadcast an INC event to other napplets via the shell |
| on(topic, callback) | { close(): void } | Subscribe to one-object IncEvent callbacks |
| channel.open(target) | Promise<ChannelHandle> | Open a symmetric point-to-point channel |
| channel.onOpened(callback) | { close(): void } | Receive inbound symmetric channel handles |
| channel.list() | Promise<ChannelInfo[]> | List active channel snapshots |
| channel.broadcast(payload?) | void | Broadcast to all open channel peers |
This non-normative SDK reference follows NAP-INC draft PR #89 at its adopted head. For outbound INC only, a queried convention URI is runtime shorthand for a stable topic plus a shallow text payload:
inc.emit('napplet:profile/open?pubkey=abc123');
// -> { type: 'inc.emit', topic: 'napplet:profile/open', payload: { pubkey: 'abc123' } }
inc.on('napplet:profile/open', (event) => {
console.log(event.sender, event.payload);
});The runtime percent-decodes the query text (+ remains +) before exact topic routing. A fragment, malformed percent encoding, repeated decoded name, or a query paired with an explicit payload throws before emission. Pass structured or non-text data through emit's explicit payload with a queryless topic. This is an INC emit input rule; subscriptions and shell routing do not parse queries or perform wildcard, prefix, or normalization matching.
Deprecated IFC compatibility exports are available as migration aliases: ifc, ifcEmit, ifcOn, IFC_DOMAIN, installIfcShim, and the Ifc* message types. They forward to the INC implementation and resolve to the canonical inc domain; new code should use inc, incEmit, incOn, INC_DOMAIN, installIncShim, and Inc* names.
intent
Archetype intent dispatch. Mirrors window.napplet.intent. Use invoke(request) or open(archetype, payload?, opts?).
import { intent } from '@napplet/sdk';
const result = await intent.open(
'profile',
{ pubkey: 'abc123' },
{ convention: 'napplet:profile/open', behavior: { newWindow: true } },
);
if (!result.handled) throw new Error(result.error);
console.log(`Handled by ${result.handler} in ${result.windowId}`);Results include required ok, archetype, action, and handled fields. IntentBehavior supports focus, newWindow, and reuse. This non-normative reference follows the living NAP-INTENT document.
storage
Sandboxed key-value storage. Mirrors window.napplet.storage. 512 KB quota per napplet.
| Method | Returns | Description |
|--------|---------|-------------|
| getItem(key) | Promise<string \| null> | Retrieve a stored value |
| setItem(key, value) | Promise<void> | Store a key-value pair |
| removeItem(key) | Promise<void> | Remove a stored key |
| keys() | Promise<string[]> | List all stored keys |
| instance.getItem/setItem/removeItem/keys | (same as above) | Per-instance storage scope — same surface, scoped to this napplet instance (sets scope: "instance" on the wire). See NAP-STORAGE. |
media
Ownership-aware media sessions. Napplet-owned sessions let your app play media and report state to the shell; shell-owned sessions provide a source so the shell fetches, plays, and reports state back.
| Method | Returns | Description |
|--------|---------|-------------|
| createSession(options) | Promise<{ sessionId?, owner?, error? }> | Create a napplet- or shell-owned media session |
| updateSession(sessionId, metadata) | void | Update metadata for an existing session |
| destroySession(sessionId) | void | Destroy a session |
| reportState(sessionId, state) | void | Report playback state |
| reportCapabilities(sessionId, actions) | void | Declare supported media actions |
| sendCommand(sessionId, action, value?) | void | Request a control action from the current playback owner |
| onCommand(sessionId, callback) | { close(): void } | Listen for shell media commands |
| onState(sessionId, callback) | { close(): void } | Listen for shell-reported state on shell-owned sessions |
| onCapabilities(sessionId, callback) | { close(): void } | Listen for shell-reported capabilities on shell-owned sessions |
| onControls(sessionId, callback) | { close(): void } | Listen for the shell's supported control list |
notify
Shell-rendered notifications. Mirrors window.napplet.notify.
| Method | Returns | Description |
|--------|---------|-------------|
| send(notification) | Promise<{ notificationId }> | Send a notification to the shell |
| dismiss(notificationId) | void | Dismiss a notification |
| badge(count) | void | Set badge count (0 to clear) |
| registerChannel(channel) | void | Register a notification channel |
| 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 |
| onControls(callback) | { close(): void } | Listen for shell's notification capabilities |
config
Per-napplet declarative configuration (NAP-CONFIG). Mirrors window.napplet.config.
| Method | Returns | Description |
|--------|---------|-------------|
| get() | Promise<Record<string, unknown>> | One-shot snapshot of validated + defaulted config values |
| subscribe(callback) | { close(): void } | Live push stream (initial snapshot + updates on change) |
| openSettings(options?) | void | Open shell's settings UI, optionally deep-linked to x-napplet-section |
| registerSchema(schema, version?) | Promise<void> | Runtime schema registration (escape hatch; prefer vite-plugin configSchema) |
| onSchemaError(callback) | () => void | Listen for config.schemaError pushes (returns plain teardown fn) |
| schema (accessor) | Record<string, unknown> \| null | Readonly current schema |
FromSchema type inference (NAP-CONFIG)
json-schema-to-ts is declared as an optional peerDependency of @napplet/nap (scoped to the @napplet/nap/config domain's FromSchema typing). Install it in your napplet to get FromSchema<typeof schema> typing for your config.subscribe callback -- the values parameter is inferred directly from your schema (enums, required fields, defaults all flow through). Authors who skip json-schema-to-ts pay no install cost and config.subscribe still works with the default Record<string, unknown> typing.
import { config } from '@napplet/sdk';
import type { FromSchema } from 'json-schema-to-ts';
const schema = {
type: 'object',
properties: {
theme: { type: 'string', enum: ['light', 'dark'], default: 'dark' },
},
required: ['theme'],
} as const;
type MyConfig = FromSchema<typeof schema>;
const sub = config.subscribe((values: MyConfig) => {
// values.theme is typed 'light' | 'dark'
});Install the peer when you want typed callbacks:
npm install --save-dev json-schema-to-tsresource
Sandboxed byte fetching (NAP-RESOURCE). Mirrors window.napplet.resource. Required because the iframe sandbox + strict CSP block direct fetch() / <img src=externalUrl> / XMLHttpRequest.
| Method | Returns | Description |
|--------|---------|-------------|
| info() | Promise<ResourceInfo> | Inspect advisory schemes and coarse policy limits. Not required before fetching. |
| bytes(url, opts?) | Promise<Blob> | Fetch bytes through the shell. opts.signal accepts an AbortSignal. |
| bytesMany(urls, opts?) | Promise<ResourceBytesItem[]> | Fetch many URLs through one envelope. Items preserve input order and length. |
| bytesAsObjectURL(url) | { url: string; revoke: () => void } | Synchronous handle whose url resolves to a blob URL once the fetch completes. |
Canonical schemes: data: (in-shim), https: (shell-side under policy), blossom:sha256:<hex> (hash-verified), htree: (Hashtree-verified), nostr:<bech32> (single-hop NIP-19).
Bare helper aliases are also re-exported for consumers that prefer functional imports:
import { resourceInfo, resourceBytes, resourceBytesMany, resourceBytesAsObjectURL } from '@napplet/sdk';
const info = await resourceInfo();
const blob = await resourceBytes('https://example.com/avatar.png');
const items = await resourceBytesMany(['https://example.com/a.png']);
const handle = resourceBytesAsObjectURL('blossom:sha256:...');keys
Keyboard forwarding and action keybindings. Mirrors window.napplet.keys.
| Method | Returns | Description |
|--------|---------|-------------|
| registerAction(action) | Promise<{ actionId, binding? }> | Declare a named action the shell can bind to a key |
| unregisterAction(actionId) | void | Remove a previously registered action |
| onAction(actionId, callback) | { close(): void } | Register a local handler for a bound key (zero-latency, not a wire message) |
identity
Read-only user identity queries (NAP-IDENTITY). Use the exported identity object, window.napplet.identity.* directly after runtime injection, or the bare-name helpers below.
| Method | Returns | Description |
|--------|---------|-------------|
| identity.getPublicKey() | Promise<string> | Shell-user pubkey, or "" when no user/signer is connected |
| identity.onChanged(handler) | { close(): void } | Listen for shell-pushed identity changes; handler receives a pubkey or "" |
Bare helper aliases are also re-exported for consumers that prefer functional imports:
import { identity, identityGetPublicKey, identityOnChanged } from '@napplet/sdk';
const pubkey = await identity.getPublicKey();
const sub = identityOnChanged((nextPubkey) => {
console.log(nextPubkey || 'signed out');
});NAP-IDENTITY is strictly read-only. Signing remains delegated through outbox.publish() or higher-level action domains (common, lists, dm). Use relay.publish() only for explicit low-level relay escape hatches. Identity changes arrive through identity.changed rather than polling.
Runtime-Injected Domains
NIP-5D runtimes inject window.napplet before napplet code runs. Available NAP domains are present as properties; unavailable domains are absent. Gate optional behavior with property presence:
if (window.napplet?.outbox) { /* outbox API is available */ }
if (window.napplet?.relay) { /* low-level relay API is available */ }
if (window.napplet?.identity) { /* identity API is available */ }
if (window.napplet?.inc) { /* inter-napplet channel API is available */ }Namespace Import
import * as napplet from '@napplet/sdk' produces an object structurally identical to window.napplet:
import * as napplet from '@napplet/sdk';
const { events } = await napplet.outbox.query([{ kinds: [1], limit: 20 }]);
napplet.storage.setItem('key', 'value');
napplet.config.subscribe((v) => console.log(v));Types
All protocol types are re-exported from @napplet/core and the NAP packages:
import type {
// Protocol types (from @napplet/core)
NostrEvent,
NostrFilter,
Subscription,
EventTemplate,
NappletMessage,
NapDomain,
// NAP message types (re-exported from NAP packages)
RelayNapMessage,
IdentityNapMessage,
StorageNapMessage,
IncNapMessage,
KeysNapMessage,
BleNapMessage,
ListsNapMessage,
Action,
} from '@napplet/sdk';Core Protocol Types
| Type | Description |
|------|-------------|
| NostrEvent | Standard Nostr event object |
| NostrFilter | Relay subscription filter |
| Subscription | Handle with close() method |
| EventTemplate | Unsigned event template for shell-mediated publish domains such as outbox.publish() |
| NappletMessage | Base JSON envelope type for all protocol messages |
| NapDomain | String literal union of NAP domain names |
NAP Message Types
These are discriminated union types covering all messages in each NAP domain. Useful for writing typed message handlers in shell implementations or protocol-aware code.
| Type | NAP Package | Description |
|------|-------------|-------------|
| RelayNapMessage | @napplet/nap/relay | Discriminated union of all relay domain messages |
| IdentityNapMessage | @napplet/nap/identity | Discriminated union of all identity domain messages |
| StorageNapMessage | @napplet/nap/storage | Discriminated union of all storage domain messages |
| IncNapMessage | @napplet/nap/inc | Discriminated union of all INC domain messages |
| KeysNapMessage | @napplet/nap/keys | Discriminated union of all keys domain messages |
| MediaNapMessage | @napplet/nap/media | Discriminated union of all media domain messages |
| NotifyNapMessage | @napplet/nap/notify | Discriminated union of all notify domain messages |
| ConfigNapMessage | @napplet/nap/config | Discriminated union of all config domain messages |
| ResourceNapMessage | @napplet/nap/resource | Discriminated union of all resource domain messages |
| BleNapMessage | @napplet/nap/ble | Discriminated union of all BLE domain messages |
| CountNapMessage | @napplet/nap/count | Discriminated union of all count domain messages |
| ListsNapMessage | @napplet/nap/lists | Discriminated union of all lists domain messages |
| CommonNapMessage | @napplet/nap/common | Discriminated union of all common domain messages |
| SerialNapMessage | @napplet/nap/serial | Discriminated union of all serial domain messages |
Individual message types (e.g., RelaySubscribeMessage, IdentityGetPublicKeyMessage) are also re-exported from @napplet/sdk for fine-grained typing.
NAP Domain Constants
Each NAP domain has a string constant re-exported from its package:
import { RELAY_DOMAIN, IDENTITY_DOMAIN, STORAGE_DOMAIN, INC_DOMAIN, THEME_DOMAIN, KEYS_DOMAIN, MEDIA_DOMAIN, NOTIFY_DOMAIN, CONFIG_DOMAIN, RESOURCE_DOMAIN, CVM_DOMAIN, OUTBOX_DOMAIN, UPLOAD_DOMAIN, INTENT_DOMAIN, BLE_DOMAIN, WEBRTC_DOMAIN, LINK_DOMAIN, COUNT_DOMAIN, LISTS_DOMAIN, COMMON_DOMAIN, SERIAL_DOMAIN, DM_DOMAIN } from '@napplet/sdk';
// Values: 'relay', 'identity', 'storage', 'inc', 'theme', 'keys', 'media', 'notify', 'config', 'resource', 'cvm', 'outbox', 'upload', 'intent', 'ble', 'webrtc', 'link', 'count', 'lists', 'common', 'serial', 'dm'These constants are re-exported from the individual domain packages. Use property presence for type-safe conditional logic:
if (window.napplet?.relay) {
// relay operations are available
}
if (window.napplet?.identity) {
// identity queries are available
}
if (window.napplet?.config) {
// NAP-CONFIG is available -- schema registration and subscribe()
}
if (window.napplet?.resource) {
// resource.bytes(url) is available.
}Runtime Guard
If window.napplet or a requested domain is unavailable when an SDK method is called, a clear error is thrown:
Error: window.napplet.relay is unavailable -- runtime did not inject this domainThis protects napplets from assuming optional domains are always present.
SDK vs Shim
| | @napplet/sdk | @napplet/shim |
|---|---|---|
| Import style | import { relay } from '@napplet/sdk' | import { installNappletGlobal } from '@napplet/shim' |
| What it does | Named exports wrapping injected domains | Runtime-side global installer |
| Dependencies | @napplet/core (types only) | None (types from @napplet/core) |
| Side effects | None | Yes -- installs globals on a target window |
| Required | Optional convenience for napplets | Runtime implementation detail |
Typical napplet usage: import the SDK for typed API access:
import { relay, inc, storage, keys, media, notify } from '@napplet/sdk';If you are writing a vanilla napplet with no build step, use the injected window.napplet.* namespace directly -- the SDK is not required.
Protocol Reference
- NIP-5D -- Napplet-shell protocol specification
- @napplet/shim -- Runtime-side injected global installer
- @napplet/core -- Shared protocol types
License
MIT
