# @kehto/shell

Browser adapter over @kehto/runtime — ShellBridge and domain proxies.

> **Alpha status:** Kehto is an early runtime toolkit for a draft NIP-5D
> protocol. NAP contracts and injected-domain behavior are still draft; treat
> this package as current implementation guidance.

## Install

```bash
pnpm add @kehto/shell
```

## Published Napplet Compatibility

`@kehto/shell` publishes against `@napplet/core` and `@napplet/nap`
`>=0.32.0 <0.33.0`. The installed core 0.32.0 / nap 0.32.0 contracts follow
NAP-INTENT authority `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`, napplet/web#199
source `3037200c932488f14f7f369b8583c39c9c16510a` / merge
`b3f0007867eac109fa4917fac9c285d3b7cc6155`, and Version Packages #198 release
source `dc1d24153c759152b6ba31a6ec9bea967798f2df`.

## Overview

`@kehto/shell` is the browser-specific integration layer for kehto. It wraps `@kehto/runtime` with the window/postMessage transport, localStorage persistence hooks, an audio manager, and the five canonical per-domain proxies defined by NIP-5D.

The primary entry point is `createShellBridge()` — it owns the postMessage listener, the NAP-SHELL `shell.ready` / `shell.init` handshake, manifest verification, and every dispatch back into the runtime engine.

Current draft behaviors this package enforces:

- The shell does not inject a host-provided nostr object into napplets — NIP-5D explicitly forbids napplet-visible signing. Napplets call `relay.publish` / `relay.publishEncrypted` and the shell mediates the signing flow internally (NIP-44 default, NIP-04 opt-in for encrypted envelopes).
- `AuthHooks.getSigner(windowId?)` receives the originating iframe window ID
  when the runtime can identify it. The shell forwards this optional context
  without imposing a consent policy; host runtimes may ignore it or use it to
  resolve their own identity-scoped signer decisions.
- `injectNappletNamespacePrelude()` implements the NIP-5D injected-domain bootstrap and mandatory NAP-SHELL shim: hosts prepend it to `srcdoc` outside verified artifact bytes, install the parent-bound `shell.init` receiver, emit one `shell.ready`, and expose callable NAP interfaces before authored scripts run. Optional namespaces are filtered to the bare-domain allowlist; `shell` is always retained. Published core 0.32.0 and shim 0.30.0 omit generic mandatory shell, so Kehto retains this host-owned prelude under NAP-SHELL `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24` until an upstream correction is reviewed.
- The injected resource projection follows draft NAP-RESOURCE `9511232f69313aa7953d110e35d32cc28d506f66`: `bytes(url, { servers? })` carries advisory Blossom locations and `bytesMany(requests)` keeps each `{ url, servers? }` entry intact.
- `window.napplet.shell.supports(domain)` answers synchronously and locally from the cached first `shell.init` environment. It returns `false` before `shell.init`, for unknown values, and for domains that are not live and granted to that napplet; it never sends a support-query message.
- Five optional per-domain proxies — `createIdentityProxy`, `createThemeProxy`, `createKeysProxy`, `createMediaProxy`, `createNotifyProxy` — can be composed between napplet and runtime to intercept request traffic per NAP. They are NOT wired by default. Identity/theme proxy `emit()` compatibility members fail closed; hosts must deliver automatic changes through `ShellBridge.publishIdentityChanged()` / `publishTheme()`, which enforce live session, granted domain, and current ACL.
- `keys.forward` is napplet-to-shell only. Active napplets suppress locally-bound keys from `keys.bindings` before forwarding; shell-initiated action triggers use `keys.action`.
- `buildShellCapabilities()` advertises only live domains. Registered `dm` and `fs` services are injected only when their handlers exist; disabled domains are absent from the delivered `domains` and named services snapshots.

### NAP-INC binding and channel contract

The injected INC binding follows merged
[`naps/NAP-INC.md`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INC.md)
on `napplet/naps` master
`5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`. The specification remains marked
draft, but its merged path is the protocol authority.

The released package projection and `window.napplet.inc` binding own
query-to-text-payload
transposition: it converts a convention URI query before emitting a stable,
exact stable convention topic identity. Subscriptions reject query- or fragment-bearing
**convention** identities; arbitrary opaque topics remain byte-for-byte exact,
including `?` and `#`. The binding never creates a normalized query-bearing
wire/discovery identity,
does not do prefix/wildcard/query-aware matching or service-over-INC prefix
dispatch, and does not infer payload kinds. Runtime delivery supplies the
**runtime-attested dTag**; no caller sender is accepted, topic source exclusion
is runtime-owned, and IDs and payloads are opaque.

Merged NAP-INC and released `@napplet/nap@0.32.0` both deliver one `IncEvent`
to `on(topic, callback)`.

For channels, runtime ACL checks are open-only rather than per-message. The
target `inc.channel.opened` before the opener result creates an equivalent target
handle; `channel.onOpened` exposes it, while symmetric handles expose `on` and
`onClosed`. The binding retains inbound, early, and terminal lifecycle state in
order, closes on bounded overflow, and treats teardown as deterministic.
`channel.list()` is informational only. The downstream tracker is
[`kehto/web#203`](https://github.com/kehto/web/issues/203), with its [upstream
resolution reply](https://github.com/kehto/web/issues/203#issuecomment-5060904495);
the prior opener-only interpretation is obsolete.

### NAP-INTENT binding and delivery

Kehto implements merged [NAP-INTENT at
`5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INTENT.md).
The protected `window.napplet.intent` binding accepts structured
`invoke(request)` calls and `open(archetype, payload?, opts?)`. It defaults
`action` to `open`, preserves an optional queryless convention, rejects
unsupported fields and caller-supplied sender data, and resolves only
parent-originated correlated results.

Successful results include `handled`, `handler`, `windowId`, and `convention`
after target dispatch completes. There is no `intent.deliver` or `onDelivery`;
selected targets consume their convention through the ordinary
runtime-attested INC binding.

Phase 105 completed released `@napplet/*` package adoption and persistent live
Paja/playground catalogs and target controllers. The Phase 104 Paja simulator
and playground catalog builder are historical exact-contract consumers; preserve
archived planning rather than presenting it as active guidance.

## Quick Start

```ts
import { createShellBridge } from '@kehto/shell';
import { createNotificationService } from '@kehto/services';

const bridge = createShellBridge({
  adapter: myShellAdapter,
  hooks: myHooks,
  target: window,
});

// Register a reference service against the underlying runtime.
bridge.runtime.registerService(
  'notify',
  createNotificationService({ onChange: updateBadge }),
);
```

## Public API

### Bridge factory
- `createShellBridge` — primary entry point; returns a `ShellBridge` (exposed `runtime`, `shell.ready`, lifecycle hooks)
- `ShellBridge` — interface type for the returned bridge

### Hooks adapter
- `adaptHooks` — convert a `ShellAdapter` + `BrowserDeps` into the canonical `RuntimeAdapter` hook bag consumed by `@kehto/runtime`

### Diagnostics
- `ShellAdapter.onUnroutedMessage?(info)` — optional observe-only hook fired when `ShellBridge.handleMessage` drops an incoming postMessage it can't route to a registered napplet window. `info` is an `UnroutedMessageInfo` (`{ type?, origin, reason }`) where `reason` is `'no-source-window'` or `'unregistered-window'`. The message is still dropped — this exists so otherwise-silent drops (e.g. an intent/`srcdoc` iframe whose `contentWindow` was never registered in `originRegistry`, or was swapped by a reload) are diagnosable instead of vanishing. Host can `console.warn` inside its own hook; the bridge adds no console output and swallows hook errors so routing is never broken.

```ts
const bridge = createShellBridge({
  ...myShellAdapter,
  onUnroutedMessage: ({ type, origin, reason }) => {
    console.warn(`[shell] dropped ${type ?? '<unknown>'} from ${origin}: ${reason}`);
  },
});
```

### Shell init
- `buildShellCapabilities` — construct the immutable domain-only `ShellCapabilities` payload emitted during the `shell.ready` / `shell.init` handshake
- `resolveShellEnvironment(hooks, identity)` — host-integrator-only utility that narrows the live environment for a trusted creation-time identity before `shell.init`; it is not installed on `window.napplet` and is not a shim-facing napplet API
- `injectNappletNamespacePrelude` — insert a host-owned NIP-5D `window.napplet` callable-domain prelude into verified HTML before authored scripts
- `renderNappletNamespacePrelude` — render only the bootstrap `<script>` for hosts that already own HTML insertion

### Domain proxies (NIP-5D composition seams)
- `createIdentityProxy` — intercept identity read requests; direct `emit()` is prohibited
- `createThemeProxy` — intercept `theme.get`; direct `emit()` is prohibited
- `createKeysProxy` — intercept `keys.bind/unbind/bindings`
- `createMediaProxy` — intercept `media.*` playback control

### Protected identity and theme delivery

The injected identity/theme bindings follow NAP-IDENTITY and NAP-THEME at
`napplet/naps` master `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`.
They are readonly protected objects: identity exposes supported reads and
`onChanged`, while theme exposes `get` and `onChanged` only. Results and
automatic changes are accepted only when their `MessageEvent.source` is
`window.parent`; direct-domain and whole-namespace assignment cannot replace
the canonical operations.

`ShellBridge` delivers `identity.changed` and `theme.changed` exactly once per
eligible recipient: a live authenticated `shell.ready` session, its frozen
environment includes the relevant domain, and its current recipient read
capability is still granted. Identity sign-out is `pubkey: ""`. Theme updates
arrive only after the service has stored the complete color state. These are
automatic change messages, not NAP-INC/intent delivery and not a subscription
protocol. Kehto's denied/unavailable theme read policy is a complete fixed
normal result without `error`, deliberately avoiding a mixed theme/error
payload.
- `createNotifyProxy` — intercept `notify.send/list/read/dismiss`

### Session / origin registry
- `sessionRegistry` — canonical windowId ↔ verified-napplet registry singleton
- `nappKeyRegistry` — deprecated alias for `sessionRegistry`
- `originRegistry` — origin-to-windowId map used by proxies and bridge broadcasts
- `PendingUpdate` — type for pending aggregate-hash change prompts

### Manifest cache
- `manifestCache` — browser-specific manifest cache singleton
- `ManifestCacheEntry` — manifest cache entry type

### Audio manager
- `audioManager` — browser audio registry singleton (audio element pool)
- `AudioSource` — per-windowId audio source shape

### Topic constants
- `TOPICS` — canonical shell topic namespace for command routing
- `TopicKey`, `TopicValue` — typed topic lookup helpers

### Types
Exported for host-app integration: `ShellAdapter`, `ShellCapabilities`, `CapabilityHooks`, `OriginIdentity`, `RelayPoolHooks`, `RelayPoolLike`, `RelayConfigHooks`, `WindowManagerHooks`, `AuthHooks`, `ConfigHooks`, `HotkeyHooks`, `WorkerRelayHooks`, `WorkerRelayLike`, `CryptoHooks`, `DmHooks`, `UploadHooks`, `IntentHooks`, `LinkHooks`, `CommonHooks`, `ListsHooks`, `SerialHooks`, `BleHooks`, `WebrtcHooks`, `SessionEntry`, `NappKeyEntry` (deprecated), `AclEntry`, `AclCheckEvent`, `UnroutedMessageInfo`, `ServiceDescriptor`, `ServiceHandler`, `ServiceRegistry`, `NostrEvent`, `NostrFilter`, `NappletMessage`, `ConsentRequest`, and per-proxy `*Deps`/`*Proxy` interfaces.

`RelayPoolLike.publish()` may return `Promise<void>` when transport acceptance
is asynchronous. The shell adapter forwards that promise so the runtime does
not acknowledge or buffer an event before the relay operation settles.
`RelayPoolHooks.publishToScopedRelay()` may likewise return
`Promise<boolean>` so asynchronous hosts report the settled outcome.

### Enforcement re-exports (from @kehto/runtime)
`createEnforceGate`, `createNapEnforceGate`, `formatDenialReason`, plus `EnforceResult`, `EnforceConfig`, `NapEnforceConfig`, `IdentityResolver`, `AclChecker`, `NapMessage`.

### Compat re-exports (DRIFT-CORE-06)

Retained for migration consumers; new integrations should use current NIP-5D envelope types from `@napplet/core`. Slated for removal once upstream restores those exports.

Re-exported from `@kehto/runtime`: the v1.1 bus-kind enum, auth event kind, shell bridge URI constant, protocol version string, the full capability list, destructive-kind set, and the replay window seconds constant. Re-exported types cover the v1.1 capability union and bus-kind numeric union. See the typedoc API reference below for the exact identifier list.

## API Reference

Full package docs: [`docs/packages/shell.md`](../../docs/packages/shell.md).
Generated API module: `docs/api/modules/_kehto_shell.html` (run `pnpm docs:api`).

## License

MIT
