Skip to content

@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.

Install

bash
npm install @napplet/shim

Runtime 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:

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

NamespaceWhat it does
outboxOutbox-aware getEvent, query, subscribe, publish, and resolveRelays; default for normal event reads and publishes
commonProfile lookup, follow/unfollow, reactions, reports, and NIP-19 helpers
listsNIP-51 list read and mutation helpers
countCount queries through the shell
dmShell-mediated encrypted direct-message helpers
relayLow-level explicit relay proxy for relay-local escape hatches
incInter-napplet communication: emit, on
intentArchetype-based intent invocation, discovery, and availability changes
storageScoped key-value storage: getItem, setItem, removeItem, keys (512 KB quota), plus storage.instance.* for per-instance scope
keysKeyboard forwarding + action keybindings: registerAction, unregisterAction, onAction
mediaOwnership-aware media sessions: createSession, reportState, onCommand, …
notifyShell-rendered notifications: send, badge, onAction, …
identityRead-only user queries: getPublicKey, onChanged, getProfile, …
configPer-napplet declarative config: get, subscribe, openSettings, registerSchema, schema
resourceSandboxed byte fetching: info, bytes, bytesMany, bytesAsObjectURL
Domain absenceIf a property is absent, that NAP is unavailable to the napplet.

Usage

ts
import { installNappletGlobal } from '@napplet/shim';

installNappletGlobal({
  domains: ['outbox', 'storage', 'identity', 'inc', 'intent'],
});

Napplet application code then consumes injected domains:

ts
// 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:

ts
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

Released under the MIT License.