Documentation
    Preparing search index...

    Module @kehto/services

    @kehto/services

    Reference service handlers for the napplet protocol — identity, relay pool, cache, keys, media, notify, theme, link, common, lists, serial, BLE, WebRTC, and DM.

    Alpha status: Kehto is an early runtime implementation for a draft NIP-5D protocol. NAP contracts and service envelopes are not final; treat these handlers as reference implementations for the current draft.

    pnpm add @kehto/services
    

    @kehto/services publishes against @napplet/core and @napplet/nap >=0.31.0 <0.32.0. The exact installed contracts are core 0.31.1 / nap 0.31.2 from NAP-INTENT authority 5ac0490461ca6fec2f0d2e45b4835cf9bc08de24, napplet/web#199 source 3037200c932488f14f7f369b8583c39c9c16510a / merge b3f0007867eac109fa4917fac9c285d3b7cc6155, and Version Packages #198 release source dc1d24153c759152b6ba31a6ec9bea967798f2df.

    @kehto/services ships the reference implementations of the ServiceHandler contract defined by @kehto/runtime. Each factory returns an object that the runtime routes NIP-5D envelopes to by the exact message.type domain (for example, notify.create goes to the handler registered under notify).

    Host apps wire services into the runtime via runtime.registerService(name, handler). The services are browser-agnostic — they have no DOM dependency. Browser-specific behaviors (audio element pool, OS notifications) are delivered through host-supplied callbacks.

    Services are selected only by the exact message.type domain. INC topics are opaque, queryless identities matched only by exact equality, so their text must not select a service handler. The runtime attaches the sender to delivered INC events from the authenticated endpoint; services do not create INC events.

    Current draft posture:

    • The v1.1 signer service is deleted outright. Its responsibilities split into two: read-only identity lookups go through createIdentityService (getPublicKey, getRelays, getProfile, getFollows, getList, getZaps, getMutes, getBlocked, getBadges); signing happens inside the shell as part of relay.publish / relay.publishEncrypted and is never exposed to napplets.
    • createKeysService and createMediaService ship real reference backends as of v1.4 (see the dedicated sections below). createKeysService attaches a document-level keydown listener by default and delivers keys.action push envelopes to registered napplets; createMediaService mirrors session metadata and playback state to navigator.mediaSession and emits media.command push envelopes on OS transport events. Both accept a host-bridge option (HostKeysBridge / HostMediaBridge) so Electron / Tauri / native shells can swap in OS-level backends without re-implementing the wire-protocol bookkeeping.
    • createNotifyService handles direct notify.* envelopes. It is not an INC topic handler.
    • createOutboxService supports outbox.getEvent from draft NAP-OUTBOX. Single-event lookups run through shell-owned relay routing and only return events whose ID matches the request. The draft wire contract keeps outbox.query one-shot and outbox.subscribe streaming; Kehto's concrete createRelayPoolOutboxRouter additionally exposes host-side queryStream() so verified query events can arrive before asynchronous NIP-65 discovery completes.
    • createResourceService implements the draft NAP-RESOURCE request lifecycle and current result shapes. It fail-closes when no scheme is configured, checks per-identity origin grants, enforces disclosed size/bulk caps, scopes cancellation to the requesting window, drops cancelled terminal envelopes, and never forwards response headers. The host policy fetch must return byte-classified output and owns scheme-specific SSRF, redirect, integrity, and SVG handling.
    • createUploadService supports upload.info from draft NAP-UPLOAD. Hosts may expose configured rails, return URL forms, maximum bytes, and MIME type policy without requiring napplets to start an upload.
    • createConfigService implements draft NAP-CONFIG's shell-writer boundary: recursive bounded-subset schema validation, per-window schema/subscription state, deterministic defaults, invalid/orphan removal, version rollback rejection, scoped host persistence hooks, and settings-UI commits. Reads before schema registration fail closed with no-schema.
    • createDmService keeps NAP-DM request correlation, per-window subscriptions, and packaged message shapes in runtime-owned code. Adapters cover concrete transports: verified NIP-17 gift wraps and relay history via nostr-tools, structural NDR runtimes with relay hooks, and Cordn/ContextVM coordinator clients.
    • createFsService keeps NAP-FS result correlation and per-window watch identity in runtime-owned code. An injected FsBackend owns real persistence, virtual-path policy, picker mediation, byte validation, revisions, mutations, and watch observation.

    Kehto follows merged NAP-INTENT at 5ac0490461ca6fec2f0d2e45b4835cf9bc08de24. Callers invoke a stable, queryless napplet:<archetype>/<action> convention. Installed verified manifest tags produce exact { slug, convention } declarations; numbered protocol names, trailing metadata, and payload inspection do not select a handler.

    • manifestToIntentCatalogEntry() converts resolved manifest { dTag, title?, archetypes: [{ slug, convention }] } data into exact candidates with actions and conventions.
    • createCatalogIntentResolver() filters by exact convention, applies the user-owned default/chooser/explicit-authorization policy, and asks an IntentTargetController to create/focus a target and dispatch the selected convention.
    • createIntentService() validates source envelopes, uses the runtime-attested sender, returns one final canonical IntentResult, and broadcasts catalog changes through recipient-policy-aware runtime sends.

    ok: true means the selected target was ready and the convention was dispatched. The result includes handled, handler, windowId, and convention. The target receives the convention and opaque payload through one runtime-attested inc.event; there is no separate intent.deliver lifecycle.

    Paja currently exposes only an exact-contract development simulator, and the playground currently exposes only a verified-manifest catalog builder. Phase 105 completed released @napplet/* package adoption plus the persistent live catalog/controller and feed-to-profile flow. Its public Intent* types are canonical releases from @napplet/core / @napplet/nap, not a local mirror; successful results report completed target dispatch.

    import {
    createIdentityService,
    createListsService,
    createNotifyService,
    createBleService,
    createDmService,
    createNip17DmAdapter,
    createSerialService,
    createWebrtcService,
    } from '@kehto/services';

    // Identity service — read-only lookups backed by a signer adapter.
    runtime.registerService(
    'identity',
    createIdentityService({
    getSigner: () => signer,
    getProfile: (pk) => nostrClient.fetchProfile(pk),
    getFollows: (pk) => contactListCache.getFollows(pk),
    }),
    );

    // Notification service — direct NIP-5D notify.* envelopes.
    runtime.registerService(
    'notify',
    createNotifyService({
    present: ({ notificationId, message, emit }) => notificationCenter.show({
    notificationId,
    title: message.title,
    body: message.body,
    onClick: () => emit({ type: 'notify.clicked', notificationId }),
    }),
    dismiss: (_windowId, notificationId) => notificationCenter.dismiss(notificationId),
    requestPermission: (_windowId, channel) => notificationPolicy.request(channel),
    }),
    );

    // Lists service — shell-owned NIP-51 metadata and mutations.
    runtime.registerService(
    'lists',
    createListsService({
    supported: () => [{ kind: 10003, type: 'bookmarks', addressable: false }],
    add: (_list, items) => ({ ok: true, added: items.length }),
    remove: (_list, items) => ({ ok: true, removed: items.length }),
    }),
    );

    // Serial service — runtime-owned serial sessions and host-owned device access.
    runtime.registerService(
    'dm',
    createDmService({
    adapter: createNip17DmAdapter({
    ownerSecretKey: shellOwnedSecretKey,
    relayPool: shellRelayPool,
    relays: ['wss://relay.example'],
    }),
    }),
    );

    runtime.registerService(
    'serial',
    createSerialService({
    open: () => ({ session: { id: 'host-session-1', state: 'open' } }),
    write: (_sessionId, _data) => {},
    close: (_sessionId) => {},
    }),
    );

    // BLE service — runtime-owned BLE/GATT sessions and host-owned device access.
    runtime.registerService(
    'ble',
    createBleService({
    open: () => ({
    session: {
    id: 'host-ble-1',
    state: 'open',
    device: { id: 'host-device-1', name: 'Host BLE' },
    },
    }),
    services: () => [],
    read: () => [87],
    write: (_sessionId, _target, _data) => {},
    subscribe: (sessionId, target, ctx) => {
    ctx.emit({ type: 'notification', sessionId, target, data: [87] });
    },
    unsubscribe: (_sessionId, _target) => {},
    close: (_sessionId) => {},
    }),
    );

    // WebRTC service — runtime-owned sessions and host-owned signaling/transport.
    runtime.registerService(
    'webrtc',
    createWebrtcService({
    open: (request, ctx) => {
    const id = 'host-webrtc-1';
    ctx.emit({ type: 'state', sessionId: id, state: 'open' });
    return {
    session: {
    id,
    scope: request.scope,
    channel: request.channel ?? 'default',
    protocol: request.protocol,
    state: 'open',
    },
    };
    },
    send: (sessionId, payload, ctx) => {
    ctx.emit({ type: 'message', sessionId, from: 'host', payload });
    },
    close: (_sessionId) => {},
    }),
    );

    NAP-OUTBOX is still an open upstream draft (napplet/naps PR #32). Its wire outbox.query.result remains one aggregate response. Hosts using Kehto's concrete router can also consume the same bounded read incrementally without changing the napplet wire protocol:

    const stream = router.queryStream(
    [{ authors: [author], kinds: [1] }],
    { timeoutMs: 3000 },
    { event: (result) => render(result.event) },
    );

    const aggregate = await stream.result;

    When relay-list loading is asynchronous, the router opens validated relay hints or fallback relays immediately, attaches discovered author write relays to the same deduplicating collector, and applies one deadline to discovery and collection. query() remains the compatibility aggregate over this stream. options.limit applies to that aggregate; the stream receives every verified unique arrival before completion. subscribe() uses the same immediate seed fanout before adding discovered relays.

    Reference keyboard / chord backend for the keys.* NIP-5D NAP. By default attaches a single document-level keydown listener that matches incoming events against registered chord subscriptions and delivers a keys.action push envelope back to the owning napplet. Implement the HostKeysBridge interface to swap in OS-level backends (Electron globalShortcut, Tauri GlobalShortcut).

    import { createKeysService } from '@kehto/services';

    export function createKeysService(options?: KeysServiceOptions): ServiceHandler & { destroy(): void };

    destroy() detaches the document listener (or the bridge's unsubscribe handles) and clears all subscription registries. Call on shell teardown.

    Field Type Description
    onForward (event: { key, code, ctrlKey, altKey, shiftKey, metaKey }) => void Called on keys.forward envelopes and on document/host-bridge keydowns handled by the reference backend. DOM-shape payload (the service translates from the wire-format { ctrl, alt, shift, meta } before invoking this callback). Forwarded keys do not dispatch keys.action; active napplets suppress bound keys locally from keys.bindings before forwarding.
    listenerTarget EventTarget Defaults to document. Pass a fresh new EventTarget() in unit tests to isolate the listener. Ignored when hostBridge is provided.
    hostBridge HostKeysBridge Pluggable OS-bridge. When provided, the service delegates keys.registerAction to bridge.subscribe(chord, cb) and the default document listener is NOT attached.
    reservedChords ReadonlyArray<string> Optional set of shell-reserved chords (wire-format strings like 'Ctrl+Shift+K', 'Cmd+P'). Reserved chords are not assigned as napplet action bindings. Document keydowns that match a reserved chord call onForward but do not dispatch keys.action. Precedence: reserved > registered. Normalized once at construction via the same parser used for action.defaultKey. See Reserved Chords.

    Copy the contract verbatim for host-app implementers. OS-level bridges implement subscribe at minimum; the two optional fields enable global-hotkey registration (works even when the host window is unfocused).

    export interface HostKeysBridge {
    /**
    * Subscribe a callback to a chord. Returns an unsubscribe handle.
    *
    * Implementations MUST:
    * - invoke `callback` exactly once per matching chord event (implementations
    * are responsible for any OS-autorepeat filtering)
    * - invoke `callback` synchronously during the event delivery
    * - accept the string chord format documented by @napplet/nap/keys
    * (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)
    */
    subscribe(chord: string, callback: (event: KeyboardEvent | HostKeyEvent) => void): () => void;

    /**
    * Optional: register an OS-level global hotkey (works even when the host
    * window is not focused). Returns true on success, false if the chord
    * cannot be registered (e.g. already claimed by another app).
    *
    * Omitted by the browser reference implementation — browsers cannot
    * register OS-level global hotkeys without privileged APIs. Electron
    * (`globalShortcut`) and Tauri (`GlobalShortcut`) provide this.
    */
    registerGlobalHotkey?(chord: string): boolean;

    /**
    * Optional: subscribe to OS-level global hotkey events (regardless of
    * focus). Returns an unsubscribe handle.
    *
    * Omitted by the browser reference implementation. See
    * {@link HostKeysBridge.registerGlobalHotkey}.
    */
    onGlobalHotkey?(callback: (chord: string) => void): () => void;
    }

    Default browser path — the reference document-level chord listener:

    import { createKeysService } from '@kehto/services';

    const keys = createKeysService({
    onForward: (event) => {
    // DOM-shape payload: { key, code, ctrlKey, altKey, shiftKey, metaKey }
    hotkeyDispatcher.dispatch(event);
    },
    });

    runtime.registerService('keys', keys);
    // On shell teardown:
    keys.destroy();

    Custom bridge path — swap in Electron's globalShortcut:

    import { createKeysService, type HostKeysBridge } from '@kehto/services';
    import { globalShortcut } from 'electron';

    const electronBridge: HostKeysBridge = {
    subscribe(chord, cb) {
    globalShortcut.register(chord, () => cb({
    key: '', code: '',
    ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
    } as KeyboardEvent));
    return () => globalShortcut.unregister(chord);
    },
    };

    runtime.registerService('keys', createKeysService({ hostBridge: electronBridge }));

    Plug a HostKeysBridge when the default document listener is insufficient: Electron or Tauri apps that need to register OS-level global hotkeys (chords delivered even when the host window is not focused), native shells that route chords through a platform-specific hotkey manager (macOS Carbon, Linux X11 grab, Windows RegisterHotKey), or test harnesses that inject synthetic events through a controlled EventTarget. The bridge owns subscription lifecycle; the service retains per-window bookkeeping (so onWindowDestroyed cleanup stays identical across paths).

    A napplet drives this end to end via @napplet/sdkkeys.registerAction to claim a chord and keys.onAction to receive dispatches against the real backend. After a successful bound registration, the service pushes a complete keys.bindings list for that napplet/window, using entries shaped as { actionId, key }. It pushes the complete list again after keys.unregisterAction removes a binding, including an empty list when no bindings remain. Injected shims use that list to suppress locally-bound keydowns before forwarding.

    Shell-reserved chords let a host application (window manager, launcher shell, tiling WM) claim specific chords for its own dispatch regardless of what napplets subscribe to. Declare the reserved set once at service construction via the reservedChords option on KeysServiceOptions:

    import { createKeysService } from '@kehto/services';

    const keys = createKeysService({
    reservedChords: [
    'Ctrl+Alt+T', // launcher
    'Super+Space', // workspace switch
    'Ctrl+Shift+Y', // shell palette
    ],
    onForward: (event) => {
    // The shell's WM dispatcher — fires for reserved chords regardless of
    // which napplet (if any) tried to register them.
    wmLauncher.dispatch(event);
    },
    });

    runtime.registerService('keys', keys);

    Precedence contract: reserved > binding. When a napplet registers an action whose defaultKey matches reservedChords, the service acknowledges the action with keys.registerAction.result but leaves binding undefined and does not add the chord to keys.bindings. That prevents the active napplet from suppressing a shell-owned chord locally.

    • If a document keydown matches a reserved chord: onForward fires exactly once and no keys.action envelope is dispatched.
    • If a document keydown matches a non-reserved registered binding: onForward fires and every napplet whose registered action matches receives a keys.action envelope via its captured send handle.
    • If a napplet sends keys.forward: onForward fires and the service does not dispatch keys.action. Active napplets are expected to suppress bound keys locally from keys.bindings before forwarding.

    Reserved chords are normalized at service construction via the same parser used for action.defaultKey, so 'Ctrl+Shift+K', 'Control+shift+k', and 'ctrl+Shift+K' all match the same chord. Modifier aliases (Cmd / Command / Win / Super → meta; Control → ctrl; Option → alt) are recognized case-insensitively.

    WM-launcher integration example:

    // Shell-side: declare every WM-absolute chord at boot.
    const keys = createKeysService({
    reservedChords: Object.keys(wmChordMap), // e.g. ['Super+1', 'Super+2', ..., 'Ctrl+Alt+T']
    onForward: (event) => {
    const chordStr = chordStringFromEvent(event);
    const action = wmChordMap[chordStr];
    if (action) action.execute();
    },
    });
    runtime.registerService('keys', keys);

    // Napplet-side (hotkey-chord napplet, for example): free to request
    // `Ctrl+Shift+K` via keys.registerAction. If Ctrl+Shift+K is NOT in the shell's
    // reservedChords, the napplet receives a binding and can handle it locally while
    // active. If the shell reserves that chord, the registration result is unbound
    // and no suppress-list entry is pushed.

    Dynamic reservation is out of scope for v1.6. If a downstream shell needs runtime updates to the reserved set (e.g. "reservation depends on which workspace is active"), open an issue referencing HostKeysBridge.reserveAbsolute(chords) — the deferred extension shape. Until then, reservedChords is static at service construction.

    OS-level global hotkeys remain a separate concern. reservedChords operates at the service layer. For OS-level reservation (chord fires even when the host window is unfocused), implement HostKeysBridge.registerGlobalHotkey in your bridge — reserved chords and global hotkeys compose orthogonally.

    Reference media backend for the media.* NIP-5D NAP. By default mirrors session metadata + playback state to navigator.mediaSession via the DOM MediaSession API and installs setActionHandler callbacks that emit media.command push envelopes on OS transport events (play / pause / next / previous / seek). Implement the HostMediaBridge interface to swap in native backends (Electron bridge, MPRIS on Linux, MediaRemote on macOS).

    import { createMediaService } from '@kehto/services';

    export function createMediaService(options?: MediaServiceOptions): ServiceHandler & { destroy(): void };

    destroy() tears down the active bridge (removes setActionHandler listeners, removes the silent-audio prime element in the browser reference implementation) and clears the session registry.

    Field Type Description
    onSessionCreate (windowId, sessionId, metadata?) => void Called when a napplet creates a session.
    onState (windowId, sessionId, state) => void Called on media.state updates — high-frequency; keep handler work minimal.
    onSessionDestroy (windowId, sessionId) => void Called when a napplet destroys a session.
    onSessionUpdate (windowId, sessionId, metadata) => void Called when a napplet updates session metadata.
    onCapabilities (windowId, sessionId, actions) => void Called when a napplet declares capabilities for a session.
    mediaSessionTarget MediaSessionTarget Overrides navigator.mediaSession (used by the default bridge only). Pass a MockMediaSession in unit tests. Ignored when hostBridge is provided.
    documentTarget Document | null Overrides document (used by the default bridge only). Set to null to disable the silent-audio prime in unit tests. Ignored when hostBridge is provided.
    hostBridge HostMediaBridge Pluggable backend. When provided, the service delegates setMetadata / setPlaybackState / onAction to the bridge and skips navigator.mediaSession entirely.

    Copy the contract verbatim for host-app implementers. Native bridges implement setMetadata + setPlaybackState + onAction at minimum; the two optional fields cover active-session switching and per-session teardown.

    export interface HostMediaBridge {
    /**
    * Set the metadata displayed on the OS transport surface for a session.
    * Called on session.create (with initial metadata) and on session.update
    * (with merged metadata) whenever the session is the active session.
    * Implementations MUST be idempotent.
    */
    setMetadata(sessionId: string, metadata: MediaMetadata): void;

    /**
    * Set the playback state for a session. Called on media.state reports
    * whenever the session is the active session. State strings match
    * nap-media MediaState.status exactly. Implementations MUST be idempotent.
    */
    setPlaybackState(sessionId: string, state: 'playing' | 'paused' | 'stopped' | 'buffering'): void;

    /**
    * Subscribe to OS-level action events (user clicks play/pause/seek/next/prev
    * on the transport surface). Returns an unsubscribe handle.
    *
    * The callback receives `(sessionId, action, value?)`. `sessionId` is the
    * bridge's currently-active session (the browser impl tracks this internally
    * via setActionHandler-at-fire-time; native impls track via setActiveSession).
    * `value` is populated for `action === 'seek'` (seek target in seconds) and
    * for `action === 'volume'` (0.0-1.0). The service dispatches the resulting
    * `media.command` envelope to the owning napplet of that session.
    */
    onAction(callback: (sessionId: string, action: MediaAction, value?: number) => void): () => void;

    /**
    * Optional: notify the bridge that the active session has changed. The
    * browser reference impl uses this to switch which session's metadata/state
    * is mirrored to the singleton navigator.mediaSession and to install (or
    * clear) action handlers for the session's declared capabilities.
    *
    * The optional `actions` parameter carries the session's declared capability
    * set so the bridge can narrow which OS transport buttons are active. When
    * omitted, the bridge applies its default set. Native OS bridges that track
    * active-session state internally may omit this field entirely.
    */
    setActiveSession?(sessionId: string | null, actions?: readonly MediaAction[]): void;

    /**
    * Optional: tear down per-session resources. The browser reference impl
    * uses this to remove the silent-audio prime element when the last session
    * is destroyed. Bridges that need no per-session teardown may omit this field.
    */
    destroySession?(sessionId: string): void;
    }

    Default browser path — the reference navigator.mediaSession mirror:

    import { createMediaService } from '@kehto/services';

    const media = createMediaService({
    onSessionCreate: (windowId, sessionId, metadata) => {
    console.log(`[${windowId}] created session ${sessionId}`, metadata);
    },
    onState: (windowId, sessionId, state) => {
    nowPlaying.update(windowId, state);
    },
    });

    runtime.registerService('media', media);
    // On shell teardown:
    media.destroy();

    Custom bridge path — swap in an Electron host bridge:

    import { createMediaService, type HostMediaBridge, type MediaAction } from '@kehto/services';
    import { mediaBridge } from './electron-media-bridge';

    const electronBridge: HostMediaBridge = {
    setMetadata(sessionId, md) {
    mediaBridge.sendMetadata({ sessionId, md });
    },
    setPlaybackState(sessionId, state) {
    mediaBridge.sendPlaybackState({ sessionId, state });
    },
    onAction(cb) {
    const handler = (_: unknown, msg: { sessionId: string; action: MediaAction; value?: number }) =>
    cb(msg.sessionId, msg.action, msg.value);
    mediaBridge.onAction(handler);
    return () => mediaBridge.offAction(handler);
    },
    };

    runtime.registerService('media', createMediaService({ hostBridge: electronBridge }));

    Plug a HostMediaBridge when navigator.mediaSession is insufficient: Electron apps that need to route transport events through the main process (lock-screen integration on Windows, Now Playing integration on macOS), Linux shells that speak MPRIS over D-Bus, native mobile wrappers that forward to AVPlayer / ExoPlayer, or test harnesses that record action events without touching the DOM. The bridge owns metadata/state mirroring and OS action routing; the service retains per-session bookkeeping (sessionRegistry + per-window send handles) so media.command dispatch semantics stay identical across paths.

    A napplet drives this end to end via @napplet/nap/mediamediaCreateSession({ owner: 'napplet', ... }), mediaReportState, and mediaOnCommand against the real backend.

    Each factory returns a ServiceHandler registrable via runtime.registerService(). The bullets below note the current NIP-5D domain the handler owns and the ACL capability napplets need in order to reach it.

    • createIdentityServiceidentity.* reads (identity:read). No signing surface; shell mediates signing internally.

    createIdentityService uses getSigner() for identity.getPublicKey and identity.getRelays. Hosts may also pass optional read-only provider hooks for getProfile, getFollows, getList, getZaps, getMutes, getBlocked, and getBadges. These hooks receive the current signer pubkey (or "" when no signer is connected) and return the payload portion of the corresponding .result envelope. Kehto does not query relays itself; the hooks are for hosts that already maintain profile, contact-list, list, zap, moderation, or badge data.

    • createNotifyService — canonical direct notify.* envelopes.
    • createRelayPoolServicerelay.publish, relay.publishEncrypted, relay.subscribe fan-out (relay:read / relay:write). Publish handlers receive only runtime-signed events and settle with canonical { ok, event, eventId } or { ok: false, error } result envelopes. Subscribe adapters may pass observed relay URLs as the callback's second argument so RelayEventResult.sidecar.relayHints records provenance rather than the larger requested relay set.
    • createCacheService — offline event cache (cache:read / cache:write).
    • createCoordinatedRelay — composite service that bundles relay-pool + cache with read-through behavior and the same canonical publish result.
    • createKeysServicekeys.registerAction / keys.unregisterAction / keys.forward + keys.bindings / keys.action push envelopes (keys:forward). Document-level chord listener by default; implement the HostKeysBridge interface to swap in Electron / Tauri / OS-level backends. See Keys Service for the full contract.
    • createMediaService — owner-aware media.session.create / update / destroy / media.state / media.capabilities + media.command push envelopes (media:control). Napplet-owned sessions mirror to navigator.mediaSession by default; shell-owned creates are rejected until a host playback/fetch bridge is supplied. Implement the HostMediaBridge interface to swap in native backends. See Media Service for the full contract.
    • createBleServiceble.open, ble.services, ble.read, ble.write, ble.subscribe, ble.unsubscribe, ble.close + host-pushed ble.event envelopes. Hook contexts include ctx.emit(event), so host Bluetooth notification listeners can forward notification, state, and closed events to the requesting napplet without replacing the reference handler.
    • createWebrtcServicewebrtc.open, webrtc.send, webrtc.close + host-pushed webrtc.event envelopes. The reference handler owns only the NAP request/result bookkeeping; host bridges own signaling, SDP, ICE, peer connection lifecycle, and policy.
    • createDmServicedm.status, dm.conversations, dm.messages, dm.send, dm.subscribe, dm.unsubscribe + dm.message push envelopes (dm:read / dm:write). Runtime owns correlation, subscriptions, cleanup, and normalized message shape; adapters own protocol transport.
    • createNip17DmAdapter — concrete NIP-17 gift-wrap adapter backed by nostr-tools/nip17 and an injected relay pool. The owner secret key stays shell/runtime-owned.
    • createNdrDmAdapter / createNdrRelayTransport — structural adapter for Nostr Double Ratchet runtimes plus a relay transport bridge for app-owned nostrSubscribe / nostrFetch / nostrPublish hooks.
    • createCordnDmAdapter / createCordnRelayCoordinatorClient — structural adapter for Cordn/ContextVM clients plus a relay-backed coordinator bridge for PostGroupMessage, FetchGroupMessages, and SubscribeGroupMessages.
    • createFsService — all fs.* request/result envelopes plus scoped fs.changed pushes. It guarantees same-id results, error/success-field exclusivity, cross-window watch isolation, and window-destroy cleanup.
    • FsBackend — host contract for runtime-owned virtual paths, real picker/storage handles, bounded range I/O, atomic writes, directory mutation, and advisory watches. FsServiceError maps failures to the closed NAP-FS error set.
    • createThemeServicetheme.get plus automatic theme.changed delivery (theme:read). Returns a ThemeService with publishTheme() and getCurrentTheme() for host-side updates.
    • createIntentService — exact intent.invoke, intent.available, and intent.handlers request/result handling plus recipient-gated intent.changed.
    • manifestToIntentCatalogEntry — verified manifest archetype contracts to catalog entries.
    • createCatalogIntentResolver — exact convention selection and retained target-controller seam.

    Against NAP-IDENTITY/NAP-THEME at napplet/naps master 5ac0490461ca6fec2f0d2e45b4835cf9bc08de24, createIdentityService is readonly: identity.getPublicKey always sends one matching .result with pubkey: "" when a signer is absent or fails, and the other supported reads retain their safe primary result fields. Unknown identity actions are silent; identity changed values are host pushes, not request retries or INC/intent traffic.

    createThemeService accepts theme.get only and owns the current complete three-color theme. publishTheme() normalizes and stores that state before it invokes its one bridge callback, so an immediate theme.get observes the same value as the automatic push. There is no theme subscribe/unsubscribe protocol. For denied or unavailable runtime reads, Kehto uses the fixed non-sensitive complete normal result without error: this explicit policy reconciles the draft error-only example without a mixed theme/error extension.

    AudioSource, AudioServiceOptions, Notification, NotificationServiceOptions, IdentityServiceOptions, RelayPoolServiceOptions, CacheServiceOptions, CoordinatedRelayOptions, KeysServiceOptions, MediaServiceOptions, NotifyServiceOptions, NotifyPresentation, NotifyInteractionMessage, ThemeServiceOptions, ThemeService, IntentOpenOptions, IntentRequest, IntentResult, IntentCandidate, IntentAvailability, IntentResolver, IntentTargetController, IntentDispatchParams, IntentTargetDispatch, BleServiceOptions, BleServiceContext, WebrtcServiceOptions, WebrtcServiceContext, DmServiceOptions, DmAdapter, DmRelayPool, Nip17DmAdapterOptions, NdrDmAdapterOptions, CordnDmAdapterOptions.

    Full package docs: docs/packages/services.md. Generated API module: docs/api/modules/_kehto_services.html (run pnpm docs:api).

    MIT

    Modules

    cvm-nostr-transport