@kehto/services 0.2.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.
@@ -0,0 +1,810 @@
1
+ import { ServiceHandler, Signer } from '@kehto/runtime';
2
+ import { NostrFilter, NostrEvent } from '@napplet/core';
3
+ import { NotifySendMessage } from '@napplet/nub-notify';
4
+ import { Theme, ThemeChangedMessage } from '@napplet/nub-theme';
5
+
6
+ /**
7
+ * @kehto/services — Shared types for reference service implementations.
8
+ *
9
+ * These types describe the internal state that services manage and expose
10
+ * to the shell host via callbacks. They are NOT NIP-01 wire types.
11
+ */
12
+ /**
13
+ * An active audio source registered by a napplet.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const source: AudioSource = {
18
+ * windowId: 'win-1',
19
+ * nappletClass: 'music-player',
20
+ * title: 'Now Playing',
21
+ * muted: false,
22
+ * };
23
+ * ```
24
+ */
25
+ interface AudioSource {
26
+ /** The napplet window that registered this audio source. */
27
+ windowId: string;
28
+ /** The napplet class/type (e.g., 'music-player'). */
29
+ nappletClass: string;
30
+ /** Human-readable title for the audio source. */
31
+ title: string;
32
+ /** Whether this source is currently muted. */
33
+ muted: boolean;
34
+ }
35
+ /**
36
+ * Options for creating an audio service via createAudioService().
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const audioService = createAudioService({
41
+ * onChange: (sources) => console.log('Audio sources changed:', sources.size),
42
+ * });
43
+ * ```
44
+ */
45
+ interface AudioServiceOptions {
46
+ /**
47
+ * Called when the audio source registry changes (register, unregister,
48
+ * mute, state update). The shell host uses this to update UI.
49
+ */
50
+ onChange?: (sources: ReadonlyMap<string, AudioSource>) => void;
51
+ }
52
+ /**
53
+ * A notification created by a napplet.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const notification: Notification = {
58
+ * id: 'notif-1',
59
+ * windowId: 'win-1',
60
+ * title: 'New Message',
61
+ * body: 'You have a new message from Alice',
62
+ * read: false,
63
+ * createdAt: 1711800000,
64
+ * };
65
+ * ```
66
+ */
67
+ interface Notification {
68
+ /** Unique notification identifier (generated by the service). */
69
+ id: string;
70
+ /** The napplet window that created this notification. */
71
+ windowId: string;
72
+ /** Short notification title. */
73
+ title: string;
74
+ /** Notification body/description. */
75
+ body: string;
76
+ /** Whether the notification has been read/acknowledged. */
77
+ read: boolean;
78
+ /** Unix timestamp (seconds) when the notification was created. */
79
+ createdAt: number;
80
+ }
81
+ /**
82
+ * Options for creating a notification service via createNotificationService().
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * const notifService = createNotificationService({
87
+ * onChange: (notifications) => console.log('Notifications:', notifications.length),
88
+ * maxPerWindow: 50,
89
+ * });
90
+ * ```
91
+ */
92
+ interface NotificationServiceOptions {
93
+ /**
94
+ * Called when the notification list changes (create, dismiss, read).
95
+ * The shell host uses this to update UI (toast, badge, etc.).
96
+ */
97
+ onChange?: (notifications: readonly Notification[]) => void;
98
+ /**
99
+ * Maximum notifications to retain per window. Oldest are evicted
100
+ * when the limit is exceeded. Default: 100.
101
+ */
102
+ maxPerWindow?: number;
103
+ }
104
+
105
+ /**
106
+ * audio-service.ts — Audio source registry as a ServiceHandler.
107
+ *
108
+ * Tracks which napplet windows are producing audio. Shell hosts wire this
109
+ * into the runtime via registerService('audio', createAudioService(opts)).
110
+ * Browser-agnostic — no DOM, no window, no postMessage.
111
+ */
112
+
113
+ /**
114
+ * Create an audio service handler.
115
+ *
116
+ * The audio service is a state registry that tracks active audio sources
117
+ * per napplet window. Napplets announce audio state via `audio:*` topic
118
+ * events; the service tracks sources and can relay mute commands back.
119
+ *
120
+ * @param options - Optional configuration (onChange callback for UI updates)
121
+ * @returns A ServiceHandler to register with the runtime
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * import { createAudioService } from '@kehto/services';
126
+ *
127
+ * const audio = createAudioService({
128
+ * onChange: (sources) => {
129
+ * // Update UI with current audio sources
130
+ * for (const [windowId, source] of sources) {
131
+ * console.log(`${source.title} (${source.muted ? 'muted' : 'playing'})`);
132
+ * }
133
+ * },
134
+ * });
135
+ *
136
+ * runtime.registerService('audio', audio);
137
+ * ```
138
+ */
139
+ declare function createAudioService(options?: AudioServiceOptions): ServiceHandler;
140
+
141
+ /**
142
+ * notification-service.ts — Notification state registry as a ServiceHandler.
143
+ *
144
+ * Tracks notifications created by napplet windows. Shell hosts wire this
145
+ * into the runtime via registerService('notifications', createNotificationService(opts)).
146
+ * The shell host decides presentation (toast, badge, OS notification, etc.)
147
+ * through the onChange callback. Browser-agnostic — no DOM, no window.
148
+ */
149
+
150
+ /**
151
+ * Create a notification service handler.
152
+ *
153
+ * The notification service is a state registry that tracks notifications
154
+ * per napplet window. Napplets create and manage notifications via
155
+ * `notifications:*` topic events; the shell host controls presentation
156
+ * via the onChange callback.
157
+ *
158
+ * @param options - Optional configuration (onChange callback, maxPerWindow limit)
159
+ * @returns A ServiceHandler to register with the runtime
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * import { createNotificationService } from '@kehto/services';
164
+ *
165
+ * const notifications = createNotificationService({
166
+ * onChange: (list) => {
167
+ * const unread = list.filter(n => !n.read);
168
+ * updateBadge(unread.length);
169
+ * },
170
+ * maxPerWindow: 50,
171
+ * });
172
+ *
173
+ * runtime.registerService('notifications', notifications);
174
+ * ```
175
+ */
176
+ declare function createNotificationService(options?: NotificationServiceOptions): ServiceHandler;
177
+
178
+ /**
179
+ * identity-service.ts — NIP-5D identity nub reference service.
180
+ *
181
+ * MIGRATION from signer-service (v1.1 -> v1.2):
182
+ * - signer.getPublicKey -> identity.getPublicKey (same shell state)
183
+ * - signer.getRelays -> identity.getRelays (same shell state)
184
+ * - signer.signEvent -> DELETED (no napplet-visible path; shell signs
185
+ * internally inside relay.publish)
186
+ * - signer.nip04.encrypt/decrypt -> DELETED
187
+ * - signer.nip44.encrypt/decrypt -> DELETED (shell encrypts internally
188
+ * inside relay.publishEncrypted)
189
+ *
190
+ * See REQUIREMENTS.md DEPS-03 (Phase 15 changelog).
191
+ *
192
+ * Handles 9 identity.* request types from @napplet/nub-identity. getPublicKey
193
+ * and getRelays return real values sourced from hooks.auth.getSigner(); the
194
+ * remaining 7 (getProfile/getFollows/getList/getZaps/getMutes/getBlocked/
195
+ * getBadges) are stub-level — each returns an empty default payload with the
196
+ * spec-correct envelope shape. Host apps plug real backends via
197
+ * runtime.registerService('identity', realHandler).
198
+ */
199
+
200
+ /**
201
+ * Options for creating the identity service.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * const identityService = createIdentityService({
206
+ * getSigner: () => window.nostr ?? null,
207
+ * });
208
+ * runtime.registerService('identity', identityService);
209
+ * ```
210
+ */
211
+ interface IdentityServiceOptions {
212
+ /**
213
+ * Return the NIP-07-compatible signer (or null) used to resolve
214
+ * identity.getPublicKey / identity.getRelays. Called on every request —
215
+ * availability can change dynamically.
216
+ */
217
+ getSigner: () => Signer | null;
218
+ }
219
+ /**
220
+ * Create an identity service that handles NIP-5D identity.* envelope messages.
221
+ *
222
+ * Supports all 9 identity.* request types from @napplet/nub-identity. The two
223
+ * read-only nostr-info queries (getPublicKey, getRelays) resolve through the
224
+ * caller-supplied signer; the remaining 7 return default/empty payloads with
225
+ * spec-correct envelope shapes so napplets always receive a result envelope.
226
+ *
227
+ * @param options - Identity service configuration (getSigner)
228
+ * @returns A ServiceHandler ready for runtime.registerService('identity', handler)
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * import { createIdentityService } from '@kehto/services';
233
+ *
234
+ * const identity = createIdentityService({
235
+ * getSigner: () => mySignerAdapter,
236
+ * });
237
+ * runtime.registerService('identity', identity);
238
+ * ```
239
+ */
240
+ declare function createIdentityService(options: IdentityServiceOptions): ServiceHandler;
241
+
242
+ /**
243
+ * relay-pool-service.ts — Relay pool as a ServiceHandler.
244
+ *
245
+ * Wraps an existing relay pool implementation (subscribe, publish,
246
+ * selectRelayTier, isAvailable) as a ServiceHandler that receives
247
+ * relay NUB envelope messages and manages subscription lifecycle.
248
+ *
249
+ * Handles: relay.subscribe, relay.close, relay.publish, relay.publishEncrypted.
250
+ *
251
+ * Note on `relay.publishEncrypted`: the canonical napplet→shell path routes
252
+ * through @kehto/runtime handleRelayMessage, which performs shell-internal
253
+ * NIP-44/NIP-04 encryption via the signer, then synthesizes a relay.publish
254
+ * envelope handed to this service. The publishEncrypted branch here is a
255
+ * fallback for alternate wirings; by the time the service sees the
256
+ * envelope, content MUST already be ciphertext (the service never encrypts
257
+ * or decrypts).
258
+ */
259
+
260
+ /**
261
+ * Options for creating a relay pool service.
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * const relayPoolService = createRelayPoolService({
266
+ * subscribe: (filters, cb, urls) => myPool.subscribe(filters, cb, urls),
267
+ * publish: (event) => myPool.publish(event),
268
+ * selectRelayTier: (filters) => myPool.selectRelays(filters),
269
+ * isAvailable: () => myPool.connected,
270
+ * });
271
+ * ```
272
+ */
273
+ interface RelayPoolServiceOptions {
274
+ /**
275
+ * Subscribe to events matching filters. Returns handle with unsubscribe().
276
+ *
277
+ * @param filters - NIP-01 filter objects
278
+ * @param callback - Receives matching events or 'EOSE'
279
+ * @param relayUrls - Optional relay URL hints
280
+ * @returns Handle to cancel the subscription
281
+ */
282
+ subscribe(filters: NostrFilter[], callback: (item: NostrEvent | 'EOSE') => void, relayUrls?: string[]): {
283
+ unsubscribe(): void;
284
+ };
285
+ /**
286
+ * Publish an event to relays.
287
+ *
288
+ * @param event - The event to publish
289
+ */
290
+ publish(event: NostrEvent): void;
291
+ /**
292
+ * Select relay URLs appropriate for the given filters.
293
+ *
294
+ * @param filters - NIP-01 filter objects
295
+ * @returns Array of relay URLs
296
+ */
297
+ selectRelayTier(filters: NostrFilter[]): string[];
298
+ /**
299
+ * Whether the relay pool is available and connected.
300
+ *
301
+ * @returns true if the relay pool can handle requests
302
+ */
303
+ isAvailable(): boolean;
304
+ }
305
+ /**
306
+ * Create a relay pool service that wraps an existing relay pool
307
+ * implementation as a ServiceHandler.
308
+ *
309
+ * Handles relay.subscribe, relay.close, and relay.publish envelopes.
310
+ * Tracks subscriptions per windowId:subId for lifecycle management.
311
+ * Sets a 15-second EOSE fallback timer on each subscription.
312
+ *
313
+ * @param options - Relay pool implementation to wrap
314
+ * @returns A ServiceHandler ready for runtime.registerService('relay', handler)
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * import { createRelayPoolService } from '@kehto/services';
319
+ *
320
+ * const pool = createRelayPoolService({
321
+ * subscribe: (f, cb, urls) => applesauce.subscribe(f, cb, urls),
322
+ * publish: (e) => applesauce.publish(e),
323
+ * selectRelayTier: (f) => applesauce.getRelays(f),
324
+ * isAvailable: () => applesauce.connected,
325
+ * });
326
+ * runtime.registerService('relay', pool);
327
+ * ```
328
+ */
329
+ declare function createRelayPoolService(options: RelayPoolServiceOptions): ServiceHandler;
330
+
331
+ /**
332
+ * cache-service.ts — Local event cache as a ServiceHandler.
333
+ *
334
+ * Wraps an existing cache implementation (query, store, isAvailable)
335
+ * as a ServiceHandler that receives relay NUB envelope messages. Cache
336
+ * subscriptions are one-shot queries — relay.subscribe triggers a query
337
+ * and immediate EOSE, unlike relay pool subscriptions which stay open.
338
+ */
339
+
340
+ /**
341
+ * Options for creating a cache service.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * const cacheService = createCacheService({
346
+ * query: (filters) => myIndexedDB.query(filters),
347
+ * store: (event) => myIndexedDB.store(event),
348
+ * isAvailable: () => true,
349
+ * });
350
+ * ```
351
+ */
352
+ interface CacheServiceOptions {
353
+ /**
354
+ * Query cached events matching the given filters.
355
+ *
356
+ * @param filters - NIP-01 filter objects
357
+ * @returns Promise resolving to matching cached events
358
+ */
359
+ query(filters: NostrFilter[]): Promise<NostrEvent[]>;
360
+ /**
361
+ * Store an event in cache. Best-effort, may silently fail.
362
+ *
363
+ * @param event - The event to store
364
+ */
365
+ store(event: NostrEvent): void;
366
+ /**
367
+ * Whether the cache is available.
368
+ *
369
+ * @returns true if the cache can handle requests
370
+ */
371
+ isAvailable(): boolean;
372
+ }
373
+ /**
374
+ * Create a cache service that wraps an existing cache implementation
375
+ * as a ServiceHandler.
376
+ *
377
+ * Cache relay.subscribe subscriptions are one-shot — they query, deliver
378
+ * results, send EOSE, and are done. No long-lived subscription tracking needed.
379
+ * Cache query failures are best-effort: EOSE is sent even on failure.
380
+ *
381
+ * @param options - Cache implementation to wrap
382
+ * @returns A ServiceHandler ready for runtime.registerService('cache', handler)
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * import { createCacheService } from '@kehto/services';
387
+ *
388
+ * const cache = createCacheService({
389
+ * query: (f) => workerRelay.query(f),
390
+ * store: (e) => workerRelay.store(e),
391
+ * isAvailable: () => workerRelay.ready,
392
+ * });
393
+ * runtime.registerService('cache', cache);
394
+ * ```
395
+ */
396
+ declare function createCacheService(options: CacheServiceOptions): ServiceHandler;
397
+
398
+ /**
399
+ * coordinated-relay.ts — Composite relay + cache ServiceHandler.
400
+ *
401
+ * Combines relay pool and cache into a single service that handles
402
+ * relay.subscribe by querying both sources, deduplicating events by ID,
403
+ * and sending a unified EOSE after both sources complete.
404
+ *
405
+ * This is a convenience helper for shell implementors. Those who need
406
+ * custom coordination can write their own composite service.
407
+ */
408
+
409
+ /**
410
+ * Options for creating a coordinated relay service.
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * const relay = createCoordinatedRelay({
415
+ * relayPool: {
416
+ * subscribe: (f, cb, urls) => pool.subscribe(f, cb, urls),
417
+ * publish: (e) => pool.publish(e),
418
+ * selectRelayTier: (f) => pool.selectRelays(f),
419
+ * isAvailable: () => pool.connected,
420
+ * },
421
+ * cache: {
422
+ * query: (f) => db.query(f),
423
+ * store: (e) => db.store(e),
424
+ * isAvailable: () => db.ready,
425
+ * },
426
+ * });
427
+ * runtime.registerService('relay', relay);
428
+ * ```
429
+ */
430
+ interface CoordinatedRelayOptions {
431
+ /**
432
+ * Relay pool implementation.
433
+ * Uses the same interface as RelayPoolServiceOptions.
434
+ */
435
+ relayPool: RelayPoolServiceOptions;
436
+ /**
437
+ * Local cache implementation.
438
+ * Uses the same interface as CacheServiceOptions.
439
+ */
440
+ cache: CacheServiceOptions;
441
+ /**
442
+ * EOSE fallback timeout in milliseconds.
443
+ * Sent if relay pool doesn't respond within this time.
444
+ * Default: 15000 (15 seconds).
445
+ */
446
+ eoseTimeoutMs?: number;
447
+ }
448
+ /**
449
+ * Create a coordinated relay service that combines relay pool and cache
450
+ * into a single ServiceHandler with dedup and unified EOSE.
451
+ *
452
+ * On relay.subscribe: queries cache first, then subscribes to relay pool.
453
+ * Events are deduplicated by ID. EOSE is sent after both sources complete.
454
+ * On relay.publish: publishes to relay pool and stores in cache.
455
+ * On relay.close: cancels relay pool subscription.
456
+ *
457
+ * @param options - Relay pool and cache implementations to coordinate
458
+ * @returns A ServiceHandler ready for runtime.registerService('relay', handler)
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * import { createCoordinatedRelay } from '@kehto/services';
463
+ *
464
+ * const relay = createCoordinatedRelay({ relayPool: myPool, cache: myCache });
465
+ * runtime.registerService('relay', relay);
466
+ * ```
467
+ */
468
+ declare function createCoordinatedRelay(options: CoordinatedRelayOptions): ServiceHandler;
469
+
470
+ /**
471
+ * keys-service.ts — NIP-5D keys NUB reference service (stub-level).
472
+ *
473
+ * Handles the 3 napplet -> shell request types from @napplet/nub-keys:
474
+ * - keys.forward -> invokes options.onForward (hotkey passthrough, fire-and-forget)
475
+ * - keys.registerAction -> echoes { actionId, binding: action.defaultKey? } as .result
476
+ * - keys.unregisterAction -> fire-and-forget (no envelope)
477
+ *
478
+ * Stub-level: no real keyboard listener, no binding persistence. Host apps
479
+ * wire a real backend via runtime.registerService('keys', realHandler).
480
+ *
481
+ * Field-name translation: @napplet/nub-keys uses the compact
482
+ * { ctrl, alt, shift, meta } form on the wire; the shell's HotkeyHooks
483
+ * (packages/shell/src/types.ts) expects the DOM-compatible
484
+ * { ctrlKey, altKey, shiftKey, metaKey } form. This service performs the
485
+ * translation so callers of `onForward` see the DOM shape.
486
+ *
487
+ * Shell -> napplet push envelopes (`keys.bindings`, `keys.action`) are
488
+ * NOT emitted by this service — they are the shell-side keys forwarder's
489
+ * responsibility (DRIFT-SHELL-06, tracked under Plan 12-11 / future phase).
490
+ */
491
+
492
+ /**
493
+ * Options for creating a keys service via createKeysService().
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * const keys = createKeysService({
498
+ * onForward: (event) => {
499
+ * // event has DOM-compatible field names: ctrlKey, altKey, etc.
500
+ * hotkeyDispatcher.dispatch(event);
501
+ * },
502
+ * });
503
+ * ```
504
+ */
505
+ interface KeysServiceOptions {
506
+ /**
507
+ * Called on keys.forward. Receives the DOM-style field names
508
+ * (ctrlKey/altKey/shiftKey/metaKey) to match the shell's HotkeyHooks
509
+ * contract. The service translates from the wire shape
510
+ * ({ ctrl, alt, shift, meta }) before invoking this callback.
511
+ */
512
+ onForward?: (event: {
513
+ key: string;
514
+ code: string;
515
+ ctrlKey: boolean;
516
+ altKey: boolean;
517
+ shiftKey: boolean;
518
+ metaKey: boolean;
519
+ }) => void;
520
+ }
521
+ /**
522
+ * Create a keys service handler.
523
+ *
524
+ * The service is stub-level: it dispatches the 3 napplet -> shell keys
525
+ * request types from @napplet/nub-keys, responds with spec-correct
526
+ * envelopes, and defers real keyboard behavior to an optional
527
+ * onForward callback. No binding persistence, no real keyboard listener.
528
+ *
529
+ * @param options - Optional configuration (onForward callback)
530
+ * @returns A ServiceHandler to register with the runtime
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * import { createKeysService } from '@kehto/services';
535
+ *
536
+ * const keys = createKeysService({
537
+ * onForward: (event) => shellHotkeyDispatcher.execute(event),
538
+ * });
539
+ *
540
+ * runtime.registerService('keys', keys);
541
+ * ```
542
+ */
543
+ declare function createKeysService(options?: KeysServiceOptions): ServiceHandler;
544
+
545
+ /**
546
+ * media-service.ts — NIP-5D media NUB reference service (stub-level).
547
+ *
548
+ * Handles 5 napplet -> shell request types from @napplet/nub-media:
549
+ * media.session.create (result), media.session.update, media.session.destroy,
550
+ * media.state, media.capabilities.
551
+ *
552
+ * Stub-level: no real MediaSession API integration. Host apps wire real
553
+ * playback backends via runtime.registerService('media', realHandler).
554
+ *
555
+ * Note: this is SEPARATE from packages/services/src/audio-service.ts, which
556
+ * is the legacy ifc-topic-based audio source registry (audio:* topic events
557
+ * over ifc.emit). media-service is the canonical @napplet/nub-media NIP-5D
558
+ * path and they coexist — audio-service continues to track audio sources for
559
+ * shell UI, while media-service handles the NUB protocol envelope surface.
560
+ *
561
+ * Shell -> Napplet push types (media.command, media.controls) are handled
562
+ * by the shell adapter separately — out of scope for this service.
563
+ */
564
+
565
+ /**
566
+ * Optional host callbacks for the stub media service.
567
+ *
568
+ * Host shells can hook session creation and state updates to drive
569
+ * their own UI (e.g., now-playing banner) without replacing the
570
+ * ServiceHandler wholesale.
571
+ *
572
+ * @example
573
+ * ```ts
574
+ * const media = createMediaService({
575
+ * onSessionCreate: (windowId, sessionId, metadata) => {
576
+ * console.log(`[${windowId}] created session ${sessionId}`, metadata);
577
+ * },
578
+ * onState: (windowId, sessionId, state) => {
579
+ * nowPlaying.update(windowId, state);
580
+ * },
581
+ * });
582
+ *
583
+ * runtime.registerService('media', media);
584
+ * ```
585
+ */
586
+ interface MediaServiceOptions {
587
+ /** Called when a napplet creates a session. May inspect/record the session. */
588
+ onSessionCreate?: (windowId: string, sessionId: string, metadata?: unknown) => void;
589
+ /** Called on media.state updates — high-frequency; keep handler work minimal. */
590
+ onState?: (windowId: string, sessionId: string, state: unknown) => void;
591
+ /** Called when a napplet destroys a session. */
592
+ onSessionDestroy?: (windowId: string, sessionId: string) => void;
593
+ /** Called when a napplet updates session metadata. */
594
+ onSessionUpdate?: (windowId: string, sessionId: string, metadata: unknown) => void;
595
+ /** Called when a napplet declares capabilities for a session. */
596
+ onCapabilities?: (windowId: string, sessionId: string, actions: unknown) => void;
597
+ }
598
+ /**
599
+ * Create a stub-level media NUB service handler.
600
+ *
601
+ * Implements the 5 napplet->shell media.* request types defined in
602
+ * `@napplet/nub-media`. Only `media.session.create` produces a reply
603
+ * envelope (`media.session.create.result`) — the remaining four
604
+ * (`session.update`, `session.destroy`, `state`, `capabilities`) are
605
+ * fire-and-forget per the NUB spec.
606
+ *
607
+ * Unknown `media.*` actions produce a `<type>.error` envelope so
608
+ * napplets are never left hanging on a malformed request.
609
+ *
610
+ * @param options - Optional host callbacks for session lifecycle + state
611
+ * @returns A ServiceHandler to register with the runtime
612
+ *
613
+ * @example
614
+ * ```ts
615
+ * import { createMediaService } from '@kehto/services';
616
+ *
617
+ * const media = createMediaService();
618
+ * runtime.registerService('media', media);
619
+ * ```
620
+ */
621
+ declare function createMediaService(options?: MediaServiceOptions): ServiceHandler;
622
+
623
+ /**
624
+ * notify-service.ts — NIP-5D notify NUB reference service (stub-level).
625
+ *
626
+ * Handles the 5 napplet -> shell request types from `@napplet/nub-notify`:
627
+ * - `notify.send` -> `notify.send.result` (shell-assigned id)
628
+ * - `notify.dismiss` -> fire-and-forget
629
+ * - `notify.badge` -> fire-and-forget
630
+ * - `notify.channel.register` -> fire-and-forget
631
+ * - `notify.permission.request` -> `notify.permission.result { granted }`
632
+ *
633
+ * Stub-level: no real Notification API calls, no real channel registry.
634
+ * Host apps wire a real backend via
635
+ * `runtime.registerService('notify', realHandler)`.
636
+ *
637
+ * Coexistence note: this is SEPARATE from
638
+ * `packages/services/src/notification-service.ts`, which is the legacy
639
+ * ifc-topic-based notification registry (operates on `ifc.emit` topics
640
+ * under `notifications:*`). `notify-service.ts` is the canonical
641
+ * `@napplet/nub-notify` NIP-5D path and lives alongside the legacy module.
642
+ * If the host app registers this service via
643
+ * `runtime.registerService('notify', ...)`, @napplet/nub-notify messages
644
+ * land here; the legacy ifc-emit topic path remains untouched.
645
+ *
646
+ * Shell -> napplet push messages (`notify.action`, `notify.clicked`,
647
+ * `notify.dismissed`, `notify.controls`) are not emitted by this stub —
648
+ * they are the host app's responsibility and are deferred to a future plan.
649
+ */
650
+
651
+ /**
652
+ * Optional configuration for `createNotifyService`.
653
+ *
654
+ * @example
655
+ * ```ts
656
+ * const notify = createNotifyService({
657
+ * generateId: () => crypto.randomUUID(),
658
+ * defaultGrant: false,
659
+ * onSend: (windowId, msg) => console.log(`napplet ${windowId} sent ${msg.title}`),
660
+ * });
661
+ * runtime.registerService('notify', notify);
662
+ * ```
663
+ */
664
+ interface NotifyServiceOptions {
665
+ /**
666
+ * Generate a shell-assigned notification ID for `notify.send.result`.
667
+ * Default: a monotonically increasing `shell-<n>` counter.
668
+ */
669
+ generateId?: () => string;
670
+ /**
671
+ * Default permission grant for `notify.permission.request`.
672
+ * Host apps that want real permission prompts should replace this
673
+ * service with a real backend via `runtime.registerService`.
674
+ * Default: `true`.
675
+ */
676
+ defaultGrant?: boolean;
677
+ /**
678
+ * Called synchronously when a napplet dispatches `notify.send`.
679
+ * Intended for host-app plumbing (UI toast, logging, etc.) without
680
+ * requiring a full backend replacement.
681
+ */
682
+ onSend?: (windowId: string, payload: NotifySendMessage) => void;
683
+ }
684
+ /**
685
+ * Create a stub-level notify service handler.
686
+ *
687
+ * Answers the 5 napplet->shell request types from `@napplet/nub-notify`.
688
+ * Does NOT implement a real backend (no DOM Notification API, no channel
689
+ * registry, no permission prompt). Host apps replace this via
690
+ * `runtime.registerService('notify', realHandler)` when a real backend is
691
+ * needed.
692
+ *
693
+ * @param options - Optional service configuration (see NotifyServiceOptions)
694
+ * @returns A ServiceHandler to register with the runtime under domain `notify`
695
+ *
696
+ * @example
697
+ * ```ts
698
+ * import { createNotifyService } from '@kehto/services';
699
+ *
700
+ * const notify = createNotifyService();
701
+ * runtime.registerService('notify', notify);
702
+ * ```
703
+ */
704
+ declare function createNotifyService(options?: NotifyServiceOptions): ServiceHandler;
705
+
706
+ /**
707
+ * theme-service.ts — NIP-5D theme NUB reference service.
708
+ *
709
+ * Handles the single napplet->shell request type from @napplet/nub-theme:
710
+ * - `theme.get` -> `theme.get.result { theme }` (current theme)
711
+ *
712
+ * Exposes a host-facing `publishTheme(theme)` handle that:
713
+ * 1. Replaces the service's internal current theme.
714
+ * 2. Invokes `options.onBroadcast(envelope)` synchronously with a
715
+ * `theme.changed` envelope so the shell adapter (Plan 13-02) can
716
+ * fan-out the push to every registered napplet.
717
+ * 3. Returns the envelope so callers can use it directly if they prefer.
718
+ *
719
+ * The default theme values are centralized here and mirrored in the runtime's
720
+ * fallback path (`packages/runtime/src/runtime.ts`) — runtime does NOT import
721
+ * from @kehto/services because services depends on runtime (one-way only).
722
+ *
723
+ * Host apps replace this via `runtime.registerService('theme', realHandler)`
724
+ * when real CSS injection / storage / dark-mode logic is needed.
725
+ *
726
+ * @example
727
+ * ```ts
728
+ * import { createThemeService } from '@kehto/services';
729
+ *
730
+ * const theme = createThemeService({
731
+ * onBroadcast: (envelope) => broadcastToAllNapplets(envelope),
732
+ * });
733
+ * runtime.registerService('theme', theme.handler);
734
+ *
735
+ * // Later, when the user flips dark/light mode:
736
+ * theme.publishTheme(newTheme);
737
+ * ```
738
+ */
739
+
740
+ /**
741
+ * Configuration for `createThemeService`.
742
+ *
743
+ * @example
744
+ * ```ts
745
+ * const theme = createThemeService({
746
+ * initialTheme: { colors: { background: '#fff', text: '#000', primary: '#00f' } },
747
+ * onBroadcast: (envelope) => shellBridge.broadcastToAll(envelope),
748
+ * });
749
+ * ```
750
+ */
751
+ interface ThemeServiceOptions {
752
+ /**
753
+ * Override the default theme payload. If omitted, the service starts with
754
+ * the canonical defaults (`#0a0a0a / #e0e0e0 / #7aa2f7`).
755
+ */
756
+ initialTheme?: Theme;
757
+ /**
758
+ * Called synchronously from `publishTheme(theme)` with a `theme.changed`
759
+ * envelope. Intended for the shell adapter (Plan 13-02) to fan-out the
760
+ * push to every registered napplet via the runtime's sendToNapplet
761
+ * primitive.
762
+ *
763
+ * Keep this callback shape framework-agnostic — the service does NOT
764
+ * import any shell / browser APIs.
765
+ */
766
+ onBroadcast?: (envelope: ThemeChangedMessage) => void;
767
+ }
768
+ /**
769
+ * A theme service bundle — the ServiceHandler that handles `theme.*`
770
+ * envelopes, the host-facing `publishTheme(theme)` handle for theme-change
771
+ * broadcasts, and a `getCurrentTheme()` accessor for host-side reads.
772
+ */
773
+ interface ThemeService {
774
+ /** Register this with the runtime via `runtime.registerService('theme', handler)`. */
775
+ handler: ServiceHandler;
776
+ /**
777
+ * Publish a theme-change to the shell adapter. Updates the service's
778
+ * internal current theme, invokes `options.onBroadcast` with a
779
+ * `theme.changed` envelope, and returns the envelope.
780
+ *
781
+ * @param theme - The new theme payload
782
+ * @returns A `theme.changed` envelope (same one passed to onBroadcast)
783
+ */
784
+ publishTheme: (theme: Theme) => ThemeChangedMessage;
785
+ /** Return the current theme. Equivalent to the payload a napplet's `theme.get` would receive. */
786
+ getCurrentTheme: () => Theme;
787
+ }
788
+ /**
789
+ * Create a theme service that handles the NIP-5D `theme.*` NUB.
790
+ *
791
+ * Answers `theme.get` with the current theme (default or
792
+ * `options.initialTheme`). Exposes `publishTheme(theme)` for the host app to
793
+ * broadcast theme changes to every registered napplet — the shell adapter
794
+ * (Plan 13-02) wires `onBroadcast` to `runtime.sendToNapplet` fan-out.
795
+ *
796
+ * @param options - Optional service configuration (see ThemeServiceOptions)
797
+ * @returns A ThemeService bundle ready for `runtime.registerService('theme', service.handler)`
798
+ *
799
+ * @example
800
+ * ```ts
801
+ * import { createThemeService } from '@kehto/services';
802
+ *
803
+ * const theme = createThemeService();
804
+ * runtime.registerService('theme', theme.handler);
805
+ * theme.publishTheme({ colors: { background: '#fff', text: '#000', primary: '#00f' } });
806
+ * ```
807
+ */
808
+ declare function createThemeService(options?: ThemeServiceOptions): ThemeService;
809
+
810
+ export { type AudioServiceOptions, type AudioSource, type CacheServiceOptions, type CoordinatedRelayOptions, type IdentityServiceOptions, type KeysServiceOptions, type MediaServiceOptions, type Notification, type NotificationServiceOptions, type NotifyServiceOptions, type RelayPoolServiceOptions, type ThemeService, type ThemeServiceOptions, createAudioService, createCacheService, createCoordinatedRelay, createIdentityService, createKeysService, createMediaService, createNotificationService, createNotifyService, createRelayPoolService, createThemeService };