Appearance
@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, 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). 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/messagingNeither 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 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; the opinion is 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. |
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
createBackendfunction; what it closes over is the shell's business. - Does not rank, score or recommend.
SecurityPropertiesis rendered verbatim bydescribeProvider. - Does not translate wire types;
@napplet/core'sDm*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