# @kernlize/messaging

Messaging provider contract, registry, composite direct-message backend and an in-memory backend for the kernl shell. Pure: no relay code, no signer, no kehto dependency. Providers receive their backend constructor from the shell.

> **Network backends live in `@kernlize/core`**
>
> This package ships no backend that sends anything over a network; the only backend in it is `createMemoryBackend`. The network backends for `private`, `legacy` and `ratchet` are `createPrivateMessagesBackend`, `createLegacyMessagesBackend` and `createRatchetMessagesBackend` in [`@kernlize/core`](https://kernl.run/docs/packages/core.md#messaging-backends), because they need the shell's event store, connection pool and account signer. `createKernl` wires them by default, so all three work in the live shell while the signed-in account's signer can encrypt; `ratchet` is experimental ([Alpha status](https://kernl.run/docs/alpha-status.md#messaging-providers-in-the-live-shell)). The offline preview (`?preview=1`) runs the same providers over the in-memory backend with a scripted correspondent, so messages sent there go nowhere.

| Field | Value |
| --- | --- |
| npm | `@kernlize/messaging` |
| JSR | `@kernl/messaging` |
| Version | `0.4.0` |
| Entry | `./dist/index.js`, types `./dist/index.d.ts` |
| Dependencies | none |
| Peer dependencies | `@napplet/core >=0.37.0 <0.39.0` |
| Side effects | none |

## Install

This package is `@kernlize/messaging` on npm and `@kernl/messaging` on JSR, the same code at the same version. Once released:

```sh
npm install @kernlize/messaging
# or
deno add jsr:@kernl/messaging
```

Neither registry has a release yet, so both commands fail. During the alpha the package is built from the kernl source repository, which is private, and consumed as a workspace dependency under its npm name. The [package index](https://kernl.run/docs/packages/index.md#install) has the steps.

```json
{ "dependencies": { "@kernlize/messaging": "workspace:^" } }
```

## Purpose

One contract for every private-messaging protocol, truthful self-description instead of ranking, and one composite backend that projects every enabled provider onto the NAP `dm` domain. The concepts are in [Messaging](https://kernl.run/docs/concepts/messaging.md); the opinion is [D-009](https://kernl.run/docs/decisions.md#d-009).

## Exports

### Contract

| Export | Signature | Description |
| --- | --- | --- |
| `MessagingProvider` | interface | `{ id; name; description; properties: SecurityProperties; availability(): ProviderAvailability; createBackend(): MessagingBackend }`. `id` is the conversation-id prefix. |
| `MessagingBackend` | interface | `status()`, `conversations(query?)`, `messages(query)`, `send(request)`, `subscribe(request, onMessage)`, `unsubscribe(subscriptionId)`, `close?()`. Every method except `send` may return synchronously or a promise. Structurally kehto's `DmAdapter`. |
| `SecurityProperties` | interface | `forwardSecrecy`, `postCompromiseSecurity`, `metadataProtection: 'none' \| 'partial' \| 'strong'`, `requiresSameClient`, `groups`, `specification?`, `experimental`. |
| `ProviderAvailability` | union | `{ available: true }` or `{ available: false; reason: string }`. The reason is a bare word such as `sign-in-required`; the registry prefixes it as `unavailable:<reason>`. |
| `ProviderEntry` | interface | `{ provider; enabled: boolean; availability: ProviderAvailability }`, what `MessagingRegistry.list()` and `get()` return. `enabled` means its backend is running. |
| `MessagingPreferences` | interface | `{ enabled; order; defaultProvider?; reply; replyChoices }`, the persisted registry state. `enabled` is what the user wants on (running or waiting until it can run), `order` every registered provider with the first choice first, `defaultProvider` the first one running, `reply` a `ReplyRule`, `replyChoices` the per-person answers by public key. |
| `SavedMessagingPreferences` | type | `Pick<MessagingPreferences, 'enabled'> & Partial<MessagingPreferences>`: what an earlier session saved, including snapshots from before there was an order. |
| `ReplyRule` | union | `'ask' \| 'theirs' \| 'mine'`: what happens when a reply would leave through a provider other than the one its recipient last wrote through. |
| `ReplyPrompt` | interface | `{ peer: DmPeer; theirs: MessagingProvider; mine: MessagingProvider }`, the question put to the user. |
| `ReplyAnswer` | interface | `{ use: 'theirs' \| 'mine'; remember: 'once' \| 'conversation' \| 'always' }`. |
| `PACKAGE_NAME` | `'@kernlize/messaging'` | The package name as a constant. |

### Registry

| Export | Signature | Description |
| --- | --- | --- |
| `createMessagingRegistry` | `(options?: MessagingRegistryOptions) => MessagingRegistry` | One per shell. Owns one backend per enabled provider, restores saved preferences lazily as providers register, and hands out one stable composite backend. |
| `MessagingRegistryOptions` | interface | `{ preferences?: SavedMessagingPreferences; onChange?: (preferences: MessagingPreferences) => void; askReply?: (prompt: ReplyPrompt) => Promise<ReplyAnswer \| undefined> }`. `onChange` receives a frozen snapshot after every change; `askReply` resolving `undefined` stops the send with `denied:cancelled`. |
| `MessagingRegistry` | interface | `register(provider)`, `list()`, `get(id)`, `enable(id)`, `disable(id)`, `setDefault(id)`, `setOrder(ids)`, `setReplyRule(rule)`, `forgetReplyChoice(pubkey?)`, `preferences()`, `composite()`, `close()`. Error reasons: `invalid:duplicate-provider`, `invalid:unknown-provider`, `unavailable:<reason>`, `invalid:provider-not-enabled`. |

### Composite backend

| Export | Signature | Description |
| --- | --- | --- |
| `createCompositeBackend` | `(resolveEnabled: () => readonly EnabledBackend[], getDefault: () => string \| undefined, options?: CompositeOptions) => MessagingBackend` | One backend over every enabled provider, given in the user's order. Ids leaving it are `<providerId>:<innerId>`; ids arriving are routed by prefix. Rules for cursors, sends and subscriptions are in [Messaging](https://kernl.run/docs/concepts/messaging.md#the-composite-backend). |
| `CompositeOptions` | interface | `{ chooseReply?: (choice: ReplyChoice) => Promise<string> \| string }`: settles a one-to-one send whose planned provider is not the one the correspondent last used. It resolves the provider id to use; a rejection stops the send. |
| `ReplyChoice` | interface | `{ peer: DmPeer; theirs: string; mine: string }`, provider ids. `peer` is the correspondent as the provider they last wrote through describes them, name included when it knows one. |
| `EnabledBackend` | interface | `{ id: string; backend: MessagingBackend }`. |
| `WithProvider<T>` | type | `T & { provider?: string }`: a `DmSendRequest` that names the provider for a new conversation. |
| `namespaceId` | `(providerId: string, innerId: string) => string` | `'private:direct:ab,cd'`. |
| `splitId` | `(id: string) => NamespacedId \| undefined` | Splits on the first `:`; `undefined` when there is no prefix. |
| `NamespacedId` | interface | `{ providerId: string; innerId: string }`. |

### In-memory backend

| Export | Signature | Description |
| --- | --- | --- |
| `createMemoryBackend` | `(options?: MemoryBackendOptions) => MemoryBackend` | A complete backend in memory for tests and the shell's offline preview. Conversations are keyed `direct:<pubkey>` or `group:<pubkey>,<pubkey>` over the sorted correspondents; cursors are numeric offsets; unread is the count of `received` messages. |
| `MemoryBackendOptions` | interface | `{ owner?: string (default 'owner'); available?: boolean (default true); now?: () => number }`. `now` is in Unix seconds and defaults to the wall clock. |
| `MemoryBackend` | interface | `MessagingBackend` plus `deliver(incoming: MemoryIncoming): DmMessage` (stores a `received` message and notifies matching subscribers) and a non-optional `close()` that drops every subscriber. |
| `MemoryIncoming` | interface | `{ senderPubkey; content; conversationId?; createdAt? }`. Without `conversationId` the message lands in `direct:<senderPubkey>`. |

`send()` rejects with `invalid:no-recipients` when neither a conversation nor a recipient other than the owner is named, `invalid:unknown-conversation` for an id the backend has never seen, and `unavailable:offline` while `available` is false. Reads keep working offline.

### Providers

| Export | Signature | Description |
| --- | --- | --- |
| `createPrivateMessagesProvider` | `(deps: ProviderDeps) => MessagingProvider` | Id `private`, name "Private messages". NIP-17. |
| `createLegacyMessagesProvider` | `(deps: ProviderDeps) => MessagingProvider` | Id `legacy`, name "Legacy messages". NIP-04. |
| `createRatchetMessagesProvider` | `(deps: ProviderDeps) => MessagingProvider` | Id `ratchet`, name "Ratchet sessions". nostr-double-ratchet draft; `experimental: true`. |
| `createGroupMessagesProvider` | `(deps: ProviderDeps) => MessagingProvider` | Id `group`, name "Group sessions". Marmot (MLS, RFC 9420; supersedes NIP-EE); forward secrecy, post-compromise security, groups; `experimental: true`. |
| `defineProvider` | `(definition: ProviderDefinition, deps: ProviderDeps) => MessagingProvider` | Joins fixed copy and properties with the shell's backend constructor and availability check. The built-ins are made with it. |
| `ProviderDeps` | interface | `{ createBackend: () => MessagingBackend; available: () => ProviderAvailability }`. |
| `ProviderDefinition` | interface | `{ id; name; description; properties: SecurityProperties }`. |
| `describeProvider` | `(provider: MessagingProvider) => readonly ProviderDescriptionRow[]` | Six label/value rows for the picker, in a fixed order; `specification` is left out so no protocol name reaches the UI. |
| `ProviderDescriptionRow` | interface | `{ label: string; value: string }`. |

## Example

```ts
import { createCompositeBackend, createMemoryBackend, createMessagingRegistry, createPrivateMessagesProvider } from '@kernlize/messaging';

const backend = createMemoryBackend({ owner: 'me', now: () => 1_700_000_000 });
await backend.send({ recipients: ['alice'], content: 'hi' });
backend.deliver({ senderPubkey: 'alice', content: 'hello' });

const registry = createMessagingRegistry({ onChange: (prefs) => console.log(prefs) });
registry.register(createPrivateMessagesProvider({ createBackend: () => backend, available: () => ({ available: true }) }));
registry.enable('private');
registry.preferences().defaultProvider; // 'private': the first enabled provider in the order

const page = await registry.composite().conversations({ limit: 20 });
page.conversations[0]?.id; // 'private:direct:alice'

// the same composite, assembled by hand
createCompositeBackend(() => [{ id: 'private', backend }], () => 'private');
```

## Scope

- Holds no relay, store or signer. The shell passes a `createBackend` function; what it closes over is the shell's business.
- Does not rank, score or recommend. `SecurityProperties` is rendered verbatim by `describeProvider`.
- Does not translate wire types; `@napplet/core`'s `Dm*` types are used directly.
- Owns backend lifetimes through the registry. The composite's `close()` is a no-op by design.

## Specification

`packages/messaging/features/`: `registry.feature`, `composite.feature`, `reply.feature`, `memory-backend.feature`, `providers.feature`.

## API reference

Generated module: [`_kernlize_messaging`](https://kernl.run/docs/api/modules/_kernlize_messaging.html)
