@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.
- package/README.md +85 -0
- package/dist/index.d.ts +810 -0
- package/dist/index.js +885 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|