@kehto/services 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -18
- package/dist/cvm-nostr-transport.d.ts +97 -0
- package/dist/cvm-nostr-transport.js +286 -0
- package/dist/cvm-nostr-transport.js.map +1 -0
- package/dist/cvm-service-CXcOVQ0m.d.ts +207 -0
- package/dist/index.d.ts +1065 -76
- package/dist/index.js +1755 -236
- package/dist/index.js.map +1 -1
- package/package.json +19 -22
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @kehto/services
|
|
2
2
|
|
|
3
|
-
Reference service handlers for the napplet protocol — audio, notifications, identity, relay pool, cache, keys
|
|
3
|
+
Reference service handlers for the napplet protocol — audio, notifications, identity, relay pool, cache, keys, media, notify, theme.
|
|
4
|
+
|
|
5
|
+
> **Alpha status:** Kehto is an early runtime implementation for a draft NIP-5D
|
|
6
|
+
> protocol. NUB contracts and service envelopes are not final; treat these
|
|
7
|
+
> handlers as reference implementations for the current draft.
|
|
4
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
@@ -14,11 +18,11 @@ pnpm add @kehto/services
|
|
|
14
18
|
|
|
15
19
|
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.
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
Current draft posture:
|
|
18
22
|
|
|
19
23
|
- 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.
|
|
20
|
-
- `createKeysService` and `createMediaService`
|
|
21
|
-
- `createNotifyService` (NIP-5D `notify.*` NUB) coexists with the legacy `createNotificationService` (ifc-emit `notifications:*` channel). Both may be registered simultaneously
|
|
24
|
+
- `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.
|
|
25
|
+
- `createNotifyService` (NIP-5D `notify.*` NUB) coexists with the legacy `createNotificationService` (ifc-emit `notifications:*` channel). Both may be registered simultaneously while hosts migrate.
|
|
22
26
|
|
|
23
27
|
## Quick Start
|
|
24
28
|
|
|
@@ -45,40 +49,338 @@ runtime.registerService(
|
|
|
45
49
|
);
|
|
46
50
|
```
|
|
47
51
|
|
|
52
|
+
## Keys Service
|
|
53
|
+
|
|
54
|
+
Reference keyboard / chord backend for the `keys.*` NIP-5D NUB. 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`](#hostkeysbridge-interface) interface to swap in OS-level backends (Electron `globalShortcut`, Tauri `GlobalShortcut`).
|
|
55
|
+
|
|
56
|
+
### Factory
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createKeysService } from '@kehto/services';
|
|
60
|
+
|
|
61
|
+
export function createKeysService(options?: KeysServiceOptions): ServiceHandler & { destroy(): void };
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`destroy()` detaches the document listener (or the bridge's unsubscribe handles) and clears all subscription registries. Call on shell teardown.
|
|
65
|
+
|
|
66
|
+
### KeysServiceOptions
|
|
67
|
+
|
|
68
|
+
| Field | Type | Description |
|
|
69
|
+
|-------|------|-------------|
|
|
70
|
+
| `onForward` | `(event: { key, code, ctrlKey, altKey, shiftKey, metaKey }) => void` | Called on `keys.forward` envelopes AND on matching document keydowns. DOM-shape payload (the service translates from the wire-format `{ ctrl, alt, shift, meta }` before invoking this callback). |
|
|
71
|
+
| `listenerTarget` | `EventTarget` | Defaults to `document`. Pass a fresh `new EventTarget()` in unit tests to isolate the listener. Ignored when `hostBridge` is provided. |
|
|
72
|
+
| `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. |
|
|
73
|
+
| `reservedChords` | `ReadonlyArray<string>` | Optional set of shell-reserved chords (wire-format strings like `'Ctrl+Shift+K'`, `'Cmd+P'`). When a napplet forwards a reserved chord via `keys.forward` OR a document keydown matches a reserved chord, `onForward` (or the `hostBridge` handler) fires but `keys.action` is NOT dispatched to any napplet that registered the same chord via `keys.registerAction`. Precedence: **reserved > registered**. Normalized once at construction via the same parser used for `action.defaultKey`. See [Reserved Chords](#reserved-chords). |
|
|
74
|
+
|
|
75
|
+
### HostKeysBridge interface
|
|
76
|
+
|
|
77
|
+
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).
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
export interface HostKeysBridge {
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe a callback to a chord. Returns an unsubscribe handle.
|
|
83
|
+
*
|
|
84
|
+
* Implementations MUST:
|
|
85
|
+
* - invoke `callback` exactly once per matching chord event (implementations
|
|
86
|
+
* are responsible for any OS-autorepeat filtering)
|
|
87
|
+
* - invoke `callback` synchronously during the event delivery
|
|
88
|
+
* - accept the string chord format documented by @napplet/nub/keys
|
|
89
|
+
* (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)
|
|
90
|
+
*/
|
|
91
|
+
subscribe(chord: string, callback: (event: KeyboardEvent | HostKeyEvent) => void): () => void;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Optional: register an OS-level global hotkey (works even when the host
|
|
95
|
+
* window is not focused). Returns true on success, false if the chord
|
|
96
|
+
* cannot be registered (e.g. already claimed by another app).
|
|
97
|
+
*
|
|
98
|
+
* Omitted by the browser reference implementation — browsers cannot
|
|
99
|
+
* register OS-level global hotkeys without privileged APIs. Electron
|
|
100
|
+
* (`globalShortcut`) and Tauri (`GlobalShortcut`) provide this.
|
|
101
|
+
*/
|
|
102
|
+
registerGlobalHotkey?(chord: string): boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Optional: subscribe to OS-level global hotkey events (regardless of
|
|
106
|
+
* focus). Returns an unsubscribe handle.
|
|
107
|
+
*
|
|
108
|
+
* Omitted by the browser reference implementation. See
|
|
109
|
+
* {@link HostKeysBridge.registerGlobalHotkey}.
|
|
110
|
+
*/
|
|
111
|
+
onGlobalHotkey?(callback: (chord: string) => void): () => void;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Usage
|
|
116
|
+
|
|
117
|
+
Default browser path — the reference document-level chord listener:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { createKeysService } from '@kehto/services';
|
|
121
|
+
|
|
122
|
+
const keys = createKeysService({
|
|
123
|
+
onForward: (event) => {
|
|
124
|
+
// DOM-shape payload: { key, code, ctrlKey, altKey, shiftKey, metaKey }
|
|
125
|
+
hotkeyDispatcher.dispatch(event);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
runtime.registerService('keys', keys);
|
|
130
|
+
// On shell teardown:
|
|
131
|
+
keys.destroy();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Custom bridge path — swap in Electron's `globalShortcut`:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { createKeysService, type HostKeysBridge } from '@kehto/services';
|
|
138
|
+
import { globalShortcut } from 'electron';
|
|
139
|
+
|
|
140
|
+
const electronBridge: HostKeysBridge = {
|
|
141
|
+
subscribe(chord, cb) {
|
|
142
|
+
globalShortcut.register(chord, () => cb({
|
|
143
|
+
key: '', code: '',
|
|
144
|
+
ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
|
|
145
|
+
} as KeyboardEvent));
|
|
146
|
+
return () => globalShortcut.unregister(chord);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
runtime.registerService('keys', createKeysService({ hostBridge: electronBridge }));
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### When to plug a custom bridge
|
|
154
|
+
|
|
155
|
+
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).
|
|
156
|
+
|
|
157
|
+
See the demo: [`apps/playground/napplets/hotkey-chord/src/main.ts`](../../apps/playground/napplets/hotkey-chord/src/main.ts) (the Phase 26 end-to-end exemplar — uses `@napplet/sdk` `keys.registerAction` + `keys.onAction` against the real backend).
|
|
158
|
+
|
|
159
|
+
### Reserved Chords
|
|
160
|
+
|
|
161
|
+
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`](#keysserviceoptions):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { createKeysService } from '@kehto/services';
|
|
165
|
+
|
|
166
|
+
const keys = createKeysService({
|
|
167
|
+
reservedChords: [
|
|
168
|
+
'Ctrl+Alt+T', // launcher
|
|
169
|
+
'Super+Space', // workspace switch
|
|
170
|
+
'Ctrl+Shift+Q', // window close
|
|
171
|
+
],
|
|
172
|
+
onForward: (event) => {
|
|
173
|
+
// The shell's WM dispatcher — fires for reserved chords regardless of
|
|
174
|
+
// which napplet (if any) tried to register them.
|
|
175
|
+
wmLauncher.dispatch(event);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
runtime.registerService('keys', keys);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Precedence contract: reserved > registered.** When a napplet forwards a chord via `keys.forward` — or when the default document keydown listener matches a chord registered by a napplet via `keys.registerAction` — the service consults the reserved set first:
|
|
183
|
+
|
|
184
|
+
- If the chord IS reserved: `onForward` (or the `hostBridge`-registered handler) fires exactly once. No `keys.action` envelope is dispatched to any napplet, even if the napplet registered the identical chord. This is intentional — the shell WANTS the forward; that is why it reserved the chord.
|
|
185
|
+
- If the chord is NOT reserved: legacy behavior. `onForward` fires AND every napplet whose registered action matches receives a `keys.action` envelope via its captured `send` handle.
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
**WM-launcher integration example:**
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// Shell-side: declare every WM-absolute chord at boot.
|
|
193
|
+
const keys = createKeysService({
|
|
194
|
+
reservedChords: Object.keys(wmChordMap), // e.g. ['Super+1', 'Super+2', ..., 'Ctrl+Alt+T']
|
|
195
|
+
onForward: (event) => {
|
|
196
|
+
const chordStr = chordStringFromEvent(event);
|
|
197
|
+
const action = wmChordMap[chordStr];
|
|
198
|
+
if (action) action.execute();
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
runtime.registerService('keys', keys);
|
|
202
|
+
|
|
203
|
+
// Napplet-side (hotkey-chord napplet, for example): perfectly free to register
|
|
204
|
+
// `Ctrl+Shift+K` via keys.registerAction. If Ctrl+Shift+K is NOT in the shell's
|
|
205
|
+
// reservedChords, the napplet receives keys.action as normal. If a shell later
|
|
206
|
+
// adds Ctrl+Shift+K to its reserved set (e.g. because it now binds the chord
|
|
207
|
+
// to a WM action), the napplet's registration is silently suppressed for that
|
|
208
|
+
// chord — the shell is authoritative.
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**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.
|
|
212
|
+
|
|
213
|
+
**OS-level global hotkeys remain a separate concern.** `reservedChords` operates at the service layer — the chord must still reach the host window's focus (or be forwarded via `keys.forward`). For OS-level reservation (chord fires even when the host window is unfocused), implement [`HostKeysBridge.registerGlobalHotkey`](#hostkeysbridge-interface) in your bridge — reserved chords and global hotkeys compose orthogonally.
|
|
214
|
+
|
|
215
|
+
## Media Service
|
|
216
|
+
|
|
217
|
+
Reference media backend for the `media.*` NIP-5D NUB. 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`](#hostmediabridge-interface) interface to swap in native backends (Electron bridge, MPRIS on Linux, MediaRemote on macOS).
|
|
218
|
+
|
|
219
|
+
### Factory
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { createMediaService } from '@kehto/services';
|
|
223
|
+
|
|
224
|
+
export function createMediaService(options?: MediaServiceOptions): ServiceHandler & { destroy(): void };
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`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.
|
|
228
|
+
|
|
229
|
+
### MediaServiceOptions
|
|
230
|
+
|
|
231
|
+
| Field | Type | Description |
|
|
232
|
+
|-------|------|-------------|
|
|
233
|
+
| `onSessionCreate` | `(windowId, sessionId, metadata?) => void` | Called when a napplet creates a session. |
|
|
234
|
+
| `onState` | `(windowId, sessionId, state) => void` | Called on `media.state` updates — high-frequency; keep handler work minimal. |
|
|
235
|
+
| `onSessionDestroy` | `(windowId, sessionId) => void` | Called when a napplet destroys a session. |
|
|
236
|
+
| `onSessionUpdate` | `(windowId, sessionId, metadata) => void` | Called when a napplet updates session metadata. |
|
|
237
|
+
| `onCapabilities` | `(windowId, sessionId, actions) => void` | Called when a napplet declares capabilities for a session. |
|
|
238
|
+
| `mediaSessionTarget` | `MediaSessionTarget` | Overrides `navigator.mediaSession` (used by the default bridge only). Pass a `MockMediaSession` in unit tests. Ignored when `hostBridge` is provided. |
|
|
239
|
+
| `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. |
|
|
240
|
+
| `hostBridge` | `HostMediaBridge` | Pluggable backend. When provided, the service delegates setMetadata / setPlaybackState / onAction to the bridge and skips `navigator.mediaSession` entirely. |
|
|
241
|
+
|
|
242
|
+
### HostMediaBridge interface
|
|
243
|
+
|
|
244
|
+
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.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
export interface HostMediaBridge {
|
|
248
|
+
/**
|
|
249
|
+
* Set the metadata displayed on the OS transport surface for a session.
|
|
250
|
+
* Called on session.create (with initial metadata) and on session.update
|
|
251
|
+
* (with merged metadata) whenever the session is the active session.
|
|
252
|
+
* Implementations MUST be idempotent.
|
|
253
|
+
*/
|
|
254
|
+
setMetadata(sessionId: string, metadata: MediaMetadata): void;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Set the playback state for a session. Called on media.state reports
|
|
258
|
+
* whenever the session is the active session. State strings match
|
|
259
|
+
* nub-media MediaState.status exactly. Implementations MUST be idempotent.
|
|
260
|
+
*/
|
|
261
|
+
setPlaybackState(sessionId: string, state: 'playing' | 'paused' | 'stopped' | 'buffering'): void;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Subscribe to OS-level action events (user clicks play/pause/seek/next/prev
|
|
265
|
+
* on the transport surface). Returns an unsubscribe handle.
|
|
266
|
+
*
|
|
267
|
+
* The callback receives `(sessionId, action, value?)`. `sessionId` is the
|
|
268
|
+
* bridge's currently-active session (the browser impl tracks this internally
|
|
269
|
+
* via setActionHandler-at-fire-time; native impls track via setActiveSession).
|
|
270
|
+
* `value` is populated for `action === 'seek'` (seek target in seconds) and
|
|
271
|
+
* for `action === 'volume'` (0.0-1.0). The service dispatches the resulting
|
|
272
|
+
* `media.command` envelope to the owning napplet of that session.
|
|
273
|
+
*/
|
|
274
|
+
onAction(callback: (sessionId: string, action: MediaAction, value?: number) => void): () => void;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Optional: notify the bridge that the active session has changed. The
|
|
278
|
+
* browser reference impl uses this to switch which session's metadata/state
|
|
279
|
+
* is mirrored to the singleton navigator.mediaSession and to install (or
|
|
280
|
+
* clear) action handlers for the session's declared capabilities.
|
|
281
|
+
*
|
|
282
|
+
* The optional `actions` parameter carries the session's declared capability
|
|
283
|
+
* set so the bridge can narrow which OS transport buttons are active. When
|
|
284
|
+
* omitted, the bridge applies its default set. Native OS bridges that track
|
|
285
|
+
* active-session state internally may omit this field entirely.
|
|
286
|
+
*/
|
|
287
|
+
setActiveSession?(sessionId: string | null, actions?: readonly MediaAction[]): void;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Optional: tear down per-session resources. The browser reference impl
|
|
291
|
+
* uses this to remove the silent-audio prime element when the last session
|
|
292
|
+
* is destroyed. Bridges that need no per-session teardown may omit this field.
|
|
293
|
+
*/
|
|
294
|
+
destroySession?(sessionId: string): void;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Usage
|
|
299
|
+
|
|
300
|
+
Default browser path — the reference `navigator.mediaSession` mirror:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { createMediaService } from '@kehto/services';
|
|
304
|
+
|
|
305
|
+
const media = createMediaService({
|
|
306
|
+
onSessionCreate: (windowId, sessionId, metadata) => {
|
|
307
|
+
console.log(`[${windowId}] created session ${sessionId}`, metadata);
|
|
308
|
+
},
|
|
309
|
+
onState: (windowId, sessionId, state) => {
|
|
310
|
+
nowPlaying.update(windowId, state);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
runtime.registerService('media', media);
|
|
315
|
+
// On shell teardown:
|
|
316
|
+
media.destroy();
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Custom bridge path — swap in an Electron host bridge:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { createMediaService, type HostMediaBridge, type MediaAction } from '@kehto/services';
|
|
323
|
+
import { mediaBridge } from './electron-media-bridge';
|
|
324
|
+
|
|
325
|
+
const electronBridge: HostMediaBridge = {
|
|
326
|
+
setMetadata(sessionId, md) {
|
|
327
|
+
mediaBridge.sendMetadata({ sessionId, md });
|
|
328
|
+
},
|
|
329
|
+
setPlaybackState(sessionId, state) {
|
|
330
|
+
mediaBridge.sendPlaybackState({ sessionId, state });
|
|
331
|
+
},
|
|
332
|
+
onAction(cb) {
|
|
333
|
+
const handler = (_: unknown, msg: { sessionId: string; action: MediaAction; value?: number }) =>
|
|
334
|
+
cb(msg.sessionId, msg.action, msg.value);
|
|
335
|
+
mediaBridge.onAction(handler);
|
|
336
|
+
return () => mediaBridge.offAction(handler);
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
runtime.registerService('media', createMediaService({ hostBridge: electronBridge }));
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### When to plug a custom bridge
|
|
344
|
+
|
|
345
|
+
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.
|
|
346
|
+
|
|
347
|
+
See the demo: [`apps/playground/napplets/media-controller/src/main.ts`](../../apps/playground/napplets/media-controller/src/main.ts) (the Phase 27 end-to-end exemplar — uses `@napplet/nub/media` `mediaCreateSession({ owner: 'napplet', ... })` + `mediaReportState` + `mediaOnCommand` against the real backend).
|
|
348
|
+
|
|
48
349
|
## Public API
|
|
49
350
|
|
|
50
|
-
Each factory returns a `ServiceHandler` registrable via `runtime.registerService()`. The bullets below note the
|
|
351
|
+
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.
|
|
51
352
|
|
|
52
353
|
### Identity NUB
|
|
53
|
-
-
|
|
354
|
+
- `createIdentityService` — `identity.*` reads (`identity:read`). No signing surface; shell mediates signing internally.
|
|
54
355
|
|
|
55
356
|
### Notify NUB
|
|
56
|
-
-
|
|
57
|
-
-
|
|
357
|
+
- `createNotifyService` — canonical `notify.*` envelopes (`notify:send` / `notify:channel`).
|
|
358
|
+
- `createNotificationService` — legacy ifc-emit `notifications:*` channel; coexists with `createNotifyService` until retired.
|
|
58
359
|
|
|
59
360
|
### Relay NUB
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
361
|
+
- `createRelayPoolService` — `relay.publish`, `relay.publishEncrypted`, `relay.subscribe` fan-out (`relay:read` / `relay:write`).
|
|
362
|
+
- `createCacheService` — offline event cache (`cache:read` / `cache:write`).
|
|
363
|
+
- `createCoordinatedRelay` — composite service that bundles relay-pool + cache with read-through behavior.
|
|
63
364
|
|
|
64
|
-
### Keys NUB
|
|
65
|
-
-
|
|
365
|
+
### Keys NUB
|
|
366
|
+
- `createKeysService` — `keys.registerAction` / `keys.unregisterAction` / `keys.forward` + `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](#keys-service) for the full contract.
|
|
66
367
|
|
|
67
|
-
### Media NUB
|
|
68
|
-
-
|
|
368
|
+
### Media NUB
|
|
369
|
+
- `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](#media-service) for the full contract.
|
|
69
370
|
|
|
70
371
|
### Theme NUB
|
|
71
|
-
-
|
|
372
|
+
- `createThemeService` — `theme.get` + `theme.changed` fan-out (`theme:read`). Returns a `ThemeService` with `publishTheme()` / `setTheme()` utilities for host-side updates.
|
|
72
373
|
|
|
73
374
|
### Audio (legacy ifc-emit)
|
|
74
|
-
-
|
|
375
|
+
- `createAudioService` — `audio:*` ifc-emit topic handler. Browser-agnostic registry of per-window audio sources; host wires `onChange` to update transport UI.
|
|
75
376
|
|
|
76
377
|
### Types
|
|
77
378
|
`AudioSource`, `AudioServiceOptions`, `Notification`, `NotificationServiceOptions`, `IdentityServiceOptions`, `RelayPoolServiceOptions`, `CacheServiceOptions`, `CoordinatedRelayOptions`, `KeysServiceOptions`, `MediaServiceOptions`, `NotifyServiceOptions`, `ThemeServiceOptions`, `ThemeService`.
|
|
78
379
|
|
|
79
380
|
## API Reference
|
|
80
381
|
|
|
81
|
-
Full
|
|
382
|
+
Full package docs: [`docs/packages/services.md`](../../docs/packages/services.md).
|
|
383
|
+
Generated API module: `docs/api/modules/_kehto_services.html` (run `pnpm docs:api`).
|
|
82
384
|
|
|
83
385
|
## License
|
|
84
386
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { C as CvmTransport } from './cvm-service-CXcOVQ0m.js';
|
|
2
|
+
import '@kehto/runtime';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* cvm-nostr-transport.ts — concrete ContextVM transport for NAP-CVM.
|
|
6
|
+
*
|
|
7
|
+
* Implements {@link CvmTransport} over Nostr, exactly as validated against live
|
|
8
|
+
* ContextVM servers (e.g. Relatr):
|
|
9
|
+
*
|
|
10
|
+
* - MCP JSON-RPC messages ride in kind-25910 event `content`.
|
|
11
|
+
* - Requests are CEP-4 gift-wrapped: the inner kind-25910 event is signed with
|
|
12
|
+
* the shell's ephemeral client key, NIP-44-encrypted to the server, and
|
|
13
|
+
* placed in a kind-21059 (ephemeral) / 1059 (regular) wrap signed by a fresh
|
|
14
|
+
* random key, `p`-tagged to the server. Responses arrive the same way,
|
|
15
|
+
* `p`-tagged to the client, and are correlated by the inner JSON-RPC `id`.
|
|
16
|
+
* - Discovery reads kind-11316 (server) + kind-11317 (tools) announcements.
|
|
17
|
+
*
|
|
18
|
+
* Shipped on a separate entry (`@kehto/services/cvm-nostr-transport`) so the
|
|
19
|
+
* `nostr-tools` dependency stays out of the core `@kehto/services` bundle.
|
|
20
|
+
*
|
|
21
|
+
* The client key is ephemeral and shell-owned: napplets never see keys, relay
|
|
22
|
+
* sockets, or NIP-44 material (NAP-CVM §Security).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { createNostrCvmTransport } from '@kehto/services/cvm-nostr-transport';
|
|
27
|
+
* const transport = createNostrCvmTransport({
|
|
28
|
+
* defaultRelays: ['wss://relay.contextvm.org', 'wss://relay2.contextvm.org'],
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Minimal signed Nostr event. */
|
|
34
|
+
interface NostrEventLike {
|
|
35
|
+
id: string;
|
|
36
|
+
pubkey: string;
|
|
37
|
+
created_at: number;
|
|
38
|
+
kind: number;
|
|
39
|
+
tags: string[][];
|
|
40
|
+
content: string;
|
|
41
|
+
sig: string;
|
|
42
|
+
}
|
|
43
|
+
/** A Nostr REQ filter (subset). */
|
|
44
|
+
interface NostrFilterLike {
|
|
45
|
+
kinds?: number[];
|
|
46
|
+
authors?: string[];
|
|
47
|
+
limit?: number;
|
|
48
|
+
['#p']?: string[];
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** Subscription handle returned by the relay pool. */
|
|
52
|
+
interface CvmSubCloser {
|
|
53
|
+
close(): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Minimal relay-pool surface used by this transport — structurally satisfied
|
|
57
|
+
* by `nostr-tools` `SimplePool`. Injectable for testing.
|
|
58
|
+
*/
|
|
59
|
+
interface CvmRelayPool {
|
|
60
|
+
subscribe(relays: string[], filter: NostrFilterLike, params: {
|
|
61
|
+
onevent?: (event: NostrEventLike) => void;
|
|
62
|
+
oneose?: () => void;
|
|
63
|
+
}): CvmSubCloser;
|
|
64
|
+
publish(relays: string[], event: NostrEventLike): unknown;
|
|
65
|
+
}
|
|
66
|
+
/** Options for {@link createNostrCvmTransport}. */
|
|
67
|
+
interface NostrCvmTransportOptions {
|
|
68
|
+
/** Relays used when a server reference carries no relay hints. */
|
|
69
|
+
defaultRelays?: string[];
|
|
70
|
+
/** Default per-request timeout in milliseconds. */
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
/** Whether to CEP-4 gift-wrap requests. Default true (most servers require it). */
|
|
73
|
+
encrypt?: boolean;
|
|
74
|
+
/** Use ephemeral (kind 21059) gift wraps when encrypting. Default true. */
|
|
75
|
+
ephemeralWrap?: boolean;
|
|
76
|
+
/** Relay pool to use. Defaults to a fresh `nostr-tools` `SimplePool`. */
|
|
77
|
+
pool?: CvmRelayPool;
|
|
78
|
+
/** Client secret key (32 bytes). Defaults to a generated ephemeral key. */
|
|
79
|
+
clientSecretKey?: Uint8Array;
|
|
80
|
+
/** Client info advertised during MCP `initialize`. */
|
|
81
|
+
clientInfo?: {
|
|
82
|
+
name: string;
|
|
83
|
+
version: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a Nostr-backed ContextVM transport.
|
|
88
|
+
*
|
|
89
|
+
* @param options - Relay set, timeouts, encryption mode, and optional injected
|
|
90
|
+
* pool/keys (the injected pool + key make the transport deterministic in tests).
|
|
91
|
+
* @returns A {@link CvmTransport} plus a `dispose()` to tear down subscriptions.
|
|
92
|
+
*/
|
|
93
|
+
declare function createNostrCvmTransport(options?: NostrCvmTransportOptions): CvmTransport & {
|
|
94
|
+
dispose(): void;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export { type CvmRelayPool, type CvmSubCloser, type NostrCvmTransportOptions, type NostrEventLike, type NostrFilterLike, createNostrCvmTransport };
|