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

- **npm:** [`@napplet/shim`](https://www.npmjs.com/package/@napplet/shim)
- **JSR:** [`@napplet/shim`](https://jsr.io/@napplet/shim)
- **Source:** [packages/shim](https://github.com/napplet/web/tree/main/packages/shim)

## Maturity-controlled injection

Use the side-effect-free `@napplet/shim/prelude` entrypoint for policy-controlled hosts. The legacy root `@napplet/shim` import still installs every bundled domain for compatibility.

```ts
import { selectNapDomains } from '@napplet/core';
import { renderNappletRuntimePreludeCall } from '@napplet/shim/prelude';

const selection = selectNapDomains({
  candidates: ['storage', 'llm', 'ble'],
  optIn: ['llm'],
});
const activate = renderNappletRuntimePreludeCall({ domains: selection.domains });
```

Inline the maintained prelude artifact followed by `activate` before napplet scripts run. Enforce the same `selection.domains` set in host dispatch: hiding a shim object does not block direct `postMessage`. Candidates must already be implemented and authorized for the napplet.

Prelude helpers also accept `maturity: {}` for supported-only filtering or `maturity: { optIn: ['llm'] }` for a named exception. Omitting `maturity` preserves existing explicit allowlist behavior. An explicit install replaces an earlier legacy namespace but cannot undo earlier calls; finish host setup before napplet code executes. See the [maturity guide](https://napplet.run/docs/naps/maturity.html) for classifications, required/optional domain handling, and provenance limits.

## Install

```bash
npm install @napplet/shim
```

## Runtime export

`installNappletGlobal` installs selected domain objects onto a target window. Napplet-side code should use [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) 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:

| Namespace | What it does |
| --- | --- |
| `outbox` | Outbox-aware `getEvent`, `query`, `subscribe`, `publish`, and `resolveRelays`; default for normal event reads and publishes |
| `common` | Profile lookup, follow/unfollow, reactions, reports, and 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 for relay-local escape hatches |
| `inc` | Inter-napplet communication: `emit`, `on` |
| `intent` | Archetype-based intent invocation, discovery, and availability changes |
| `storage` | Scoped key-value storage: `getItem`, `setItem`, `removeItem`, `keys` (512 KB quota), plus `storage.instance.*` for per-instance scope |
| `keys` | Keyboard forwarding + action keybindings: `registerAction`, `unregisterAction`, `onAction` |
| `media` | Ownership-aware media sessions: `createSession`, `reportState`, `onCommand`, … |
| `notify` | Shell-rendered notifications: `send`, `badge`, `onAction`, … |
| `identity` | Read-only user queries: `getPublicKey`, `onChanged`, `getProfile`, … |
| `config` | Per-napplet declarative config: `get`, `subscribe`, `openSettings`, `registerSchema`, `schema` |
| `resource` | Sandboxed byte fetching: `info`, `bytes`, `bytesMany`, `bytesAsObjectURL` |
| Domain absence | If 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('napplet:profile/open', { payload: { pubkey: 'abc123' }, behavior: { reuse: false } });

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

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](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md).

This behavior defers to the living [NAP-INC](https://github.com/napplet/naps/blob/master/naps/NAP-INC.md) and [NAP-INTENT](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) 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`](https://napplet.run/docs/packages/core.md), or — preferably — use the named helpers in [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md):

```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 }`). `installNappletGlobal` first opens the NIP-5D channel: it creates a `MessageChannel`, keeps one port private and hands the other to the shell once in `napplet.hello`. Outbound and inbound envelopes then travel only over that port; window-level messages are ignored. 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

- [Shim vs. SDK](https://napplet.run/docs/guide/getting-started.md#when-to-use-shim-vs-sdk)
- [`@napplet/sdk`](https://napplet.run/docs/packages/sdk.md) — named, typed wrapper over the same global

### 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](https://github.com/napplet/naps/blob/master/naps/NAP-SHELL.md).

### 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](https://github.com/napplet/naps/blob/master/naps/NAP-INTENT.md) and [web projection](https://github.com/napplet/naps/blob/master/projections/web.md).

### LLM runtime binding

Select `llm` in `installNappletGlobal({ domains: ['llm'] })` or the equivalent runtime prelude options to expose `window.napplet.llm`. The binding forwards `select`, `describe`, `generate`, and `cancel`, correlates each result by `id` and type, routes `llm.delta` only to the generation that owns its `requestId`, drops deltas that do not match the Delta schema (`start` / `delta` / `end` events, `text` only on `delta`, `toolCallId` and `name` only on a `toolCall` start), stops provisional delivery on a sequence gap, rejects an `Error` record as an error carrying the specification code, and maps an abort signal to `llm.cancel`. Installing the binding supplies no connection, consent, or provider: the host provides policy-bound handlers, endpoint-scoped handles, and prompts. See the living [NAP-LLM proposal](https://github.com/napplet/naps/pull/134).
