Skip to content

@napplet/sdk ​

Named TypeScript exports for napplet developers using a bundler. Wraps window.napplet at call time.

@napplet/sdk gives napplet code typed, named imports that delegate to injected window.napplet.* counterparts. It depends on @napplet/core for types only and has no side effects. If a method is called before the runtime injected window.napplet, or before that domain is available, the SDK throws a clear error.

NAP proposal status and shipped implementation maturity are separate. All capabilities can change. Inspect NAP_IMPLEMENTATIONS from @napplet/core and use selectNapDomains for local host opt-in policy. See maturity and opt-in guidance.

Install ​

bash
npm install @napplet/sdk

Key exports ​

Top-level namespaced objects that mirror window.napplet:

  • outbox — getEvent, query, subscribe, publish, resolveRelays
  • common — profile lookup, follow/unfollow, reactions, reports, 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; use only for relay-local escape hatches
  • inc — emit, on (plus deprecated ifc* migration aliases)
  • intent — invoke, open, available, handlers, onChanged
  • storage — getItem, setItem, removeItem, keys, plus storage.instance.* (per-instance scope)
  • keys — registerAction, unregisterAction, onAction
  • media — createSession, reportState, onCommand, …
  • notify — send, badge, onAction, …
  • config — get, subscribe, openSettings, registerSchema, schema
  • llm — User-selected, runtime-mediated language model inference: select, describe, generate, cancel
  • resource — info, bytes, bytesMany, bytesAsObjectURL

identity is exported as a top-level object and through bare-name helpers:

  • identityGetPublicKey, identityOnChanged

There is no top-level shell object. Detect capability availability from runtime-injected domain presence (window.napplet?.outbox, window.napplet?.dm, and so on).

The SDK also re-exports:

  • the *_DOMAIN constants and install*Shim installers
  • resourceInfo, resourceBytes, resourceBytesMany, resourceBytesAsObjectURL

It also re-exports the protocol types from @napplet/core and the per-domain message-type unions (RelayNapMessage, IdentityNapMessage, …) and *_DOMAIN constants from the NAP packages.

Usage ​

ts
import { outbox, common, inc, intent, storage, keys, config, resource } 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: payload is a local convention choice. inc.emit('chat:message', { text: 'hi' });

const intentResult = await intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } }); if (!intentResult.ok) throw new Error(intentResult.error);

// Scoped storage await storage.setItem('theme', 'dark');

// Live per-napplet config const configSub = config.subscribe((values) => applyTheme(values.theme));

// Fetch external bytes through the shell (`blossom:<sha256>.<ext>` URIs follow BUD-10: https://github.com/hzrd149/blossom/blob/master/buds/10.md)
const avatarBlob = await resource.bytes('blossom:<sha256>.png', {
  servers: ['https://cdn.hzrd149.com'],
});
const avatarItems = await resource.bytesMany([
  { url: 'https://example.com/avatar.png' },
  { url: 'blossom:<sha256>.png', servers: ['https://cdn.hzrd149.com'] },
]);

INC convention URIs ​

inc.emit(topic, payload?) accepts a queried convention URI at the INC developer boundary. The binding transposes a call such as inc.emit('napplet:profile/open?pubkey=abc123') into the queryless stable topic napplet:profile/open plus a shallow decoded text payload. pubkey is a local convention choice; receiving code owns payload validation.

Subscribe using the stable topic, not the queried developer-facing URI:

ts
inc.emit('napplet:profile/open?pubkey=abc123');
const profileOpen = inc.on('napplet:profile/open', (event) => {
  validateProfileOpenPayload(event.payload);
});

Routing remains exact after transposition, with no query-aware, prefix, or wildcard matching. Fragments, malformed percent encoding, repeated decoded names, and a query combined with an explicit payload reject before emission. Manifest convention values and normalized wire identities stay queryless.

Intent dispatch ​

Use intent.invoke(uri, options?) or intent.open(uri, options?).

ts
const result = await intent.open('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } });
if (!result.ok) throw new Error(result.error);

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.

These APIs defer to the living NAP-INC and NAP-INTENT documents.

Typed config with FromSchema ​

json-schema-to-ts is an optional peer dependency. Install it to get FromSchema<typeof schema> typing flowing into your config.subscribe callback; skip it and config.subscribe still works with the default Record<string, unknown> typing.

ts
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;

const sub = config.subscribe((values: FromSchema<typeof schema>) => {
  // values.theme is typed 'light' | 'dark'
});

Namespace import ​

import * as napplet from '@napplet/sdk' produces an object structurally identical to window.napplet:

ts
import * as napplet from '@napplet/sdk';
const { events } = await napplet.outbox.query([{ kinds: [1], limit: 20 }]);

See also ​

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 ​

ts
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. Theme updates reach napplets with the exposed theme domain, independently of manifest requirement declarations. User identity changes report the connected signer, including an empty string at sign-out; they do not alter napplet identity. Profile picture/banner bytes use optional resource when exposed; otherwise show local placeholder artwork. Theme media URLs do not grant network access, so colors and local fonts provide the fallback. See NAP-THEME and NAP-IDENTITY.

Controller snapshots ​

This non-normative guidance defers to NAP-GAMEPAD. When the runtime exposes window.napplet.gamepad, the SDK provides cached getGamepads(), read-only available and focused, and onChange(handler) with an independently closable listener. Keep keyboard or pointer controls available when the domain or native controllers are unavailable. Ignore gameplay edges while unfocused, and reset the button baseline on regained focus. Preserve null slots and route controllers by their index.

ts
import { gamepad } from '@napplet/sdk/gamepad';

if (window.napplet?.gamepad) {
  const subscription = gamepad.onChange(state => {
    if (!state.available || !state.focused) return;
    for (const pad of state.pads) {
      if (pad) console.log(pad.index, pad.axes);
    }
  });
  // Call subscription.close() when the owning view is disposed, not right away.
}

Declare gamepad as optional (O) when other controls fulfill the core task. Use a hard requirement (R) only when the core task needs controller input. The runtime owns controller access and focus isolation; app code does not call native navigator.getGamepads() or open raw HID devices. Haptics and remapping are outside this proposal.

Language model inference ​

This non-normative guide follows the living NAP-LLM proposal. Import llm from @napplet/sdk or @napplet/sdk/llm; root exports include prefixed helpers such as llmSelect and llmGenerate. The dedicated subpath also provides bare aliases such as select and generate.

The API exposes select, describe, generate, and cancel. generate returns a pending promise that exposes requestId; pass onDelta for ordered provisional start / delta / end block events and signal to map an abort to llm.cancel, and replace provisional output with the terminal content parts. llmText(generation) concatenates the text parts; llmToolCalls(generation) returns tool calls only from a finish: "toolUse" result. Each method resolves with the proposal's result record; a runtime Error record rejects with an Error carrying the specification code, recognizable with isLlmFailure(error), while the binding's own transport timeout rejects without a code.

ts
import { llm, isLlmFailure, llmText } from '@napplet/sdk/llm';

if (window.napplet?.llm) {
  try {
    const model = await llm.select({ json: true });
    const generation = await llm.generate({
      handle: model.handle,
      messages: [{ role: 'user', content: [{ type: 'text', text: 'Return a JSON object with a greeting.' }] }],
      format: 'json',
    });
    if (generation.finish === 'stop') render(JSON.parse(llmText(generation)));
  } catch (error) {
    if (isLlmFailure(error)) console.warn(error.code, error.message);
    else throw error;
  }
}

Selection does not authorize disclosure: the runtime obtains consent before each generation and may answer user-cancelled. Only finish: "stop" is complete output. Tool calls are requests the napplet validates and executes itself, if at all, through separately authorized capabilities; the runtime never executes them, and tool results re-enter the conversation as untrusted input. Handles are endpoint-scoped and expire when the connection, account, or endpoint changes. The runtime owns connections, credentials, consent, and provider access; model output is untrusted data.

Released under the MIT License.