@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/dist/index.js ADDED
@@ -0,0 +1,885 @@
1
+ // src/audio-service.ts
2
+ var AUDIO_SERVICE_VERSION = "1.0.0";
3
+ function createAudioService(options) {
4
+ const sources = /* @__PURE__ */ new Map();
5
+ const onChange = options?.onChange;
6
+ function notify() {
7
+ onChange?.(new Map(sources));
8
+ }
9
+ const descriptor = {
10
+ name: "audio",
11
+ version: AUDIO_SERVICE_VERSION,
12
+ description: "Audio source registry \u2014 tracks active audio sources per napplet window"
13
+ };
14
+ return {
15
+ descriptor,
16
+ handleMessage(windowId, message, send) {
17
+ if (message.type !== "ifc.emit") return;
18
+ const topic = message.topic;
19
+ if (!topic?.startsWith("audio:")) return;
20
+ const action = topic.slice(6);
21
+ const payload = message.payload ?? {};
22
+ switch (action) {
23
+ case "register": {
24
+ const nappletClass = typeof payload.nappletClass === "string" ? payload.nappletClass : "";
25
+ const title = typeof payload.title === "string" ? payload.title : "";
26
+ sources.set(windowId, { windowId, nappletClass, title, muted: false });
27
+ notify();
28
+ break;
29
+ }
30
+ case "unregister": {
31
+ if (sources.delete(windowId)) {
32
+ notify();
33
+ }
34
+ break;
35
+ }
36
+ case "state-changed": {
37
+ const source = sources.get(windowId);
38
+ if (!source) return;
39
+ if (typeof payload.title === "string") {
40
+ source.title = payload.title;
41
+ }
42
+ notify();
43
+ break;
44
+ }
45
+ case "mute": {
46
+ const targetWindowId = typeof payload.windowId === "string" ? payload.windowId : windowId;
47
+ const muted = payload.muted === true;
48
+ const source = sources.get(targetWindowId);
49
+ if (source) {
50
+ source.muted = muted;
51
+ notify();
52
+ }
53
+ send({ type: "ifc.event", topic: "napplet:audio-muted", payload: { muted } });
54
+ break;
55
+ }
56
+ default:
57
+ break;
58
+ }
59
+ },
60
+ onWindowDestroyed(windowId) {
61
+ if (sources.delete(windowId)) {
62
+ notify();
63
+ }
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/notification-service.ts
69
+ var NOTIFICATION_SERVICE_VERSION = "1.0.0";
70
+ var DEFAULT_MAX_PER_WINDOW = 100;
71
+ var idCounter = 0;
72
+ function generateId() {
73
+ idCounter++;
74
+ return `notif-${Date.now()}-${idCounter}`;
75
+ }
76
+ function createNotificationService(options) {
77
+ const notifications = /* @__PURE__ */ new Map();
78
+ const onChange = options?.onChange;
79
+ const maxPerWindow = options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW;
80
+ function getAllNotifications() {
81
+ const all = [];
82
+ for (const windowNotifs of notifications.values()) {
83
+ all.push(...windowNotifs);
84
+ }
85
+ return all;
86
+ }
87
+ function notify() {
88
+ onChange?.(getAllNotifications());
89
+ }
90
+ function getWindowNotifications(windowId) {
91
+ let list = notifications.get(windowId);
92
+ if (!list) {
93
+ list = [];
94
+ notifications.set(windowId, list);
95
+ }
96
+ return list;
97
+ }
98
+ function enforceLimit(list) {
99
+ while (list.length > maxPerWindow) {
100
+ list.shift();
101
+ }
102
+ }
103
+ function findById(id) {
104
+ for (const [windowId, list] of notifications) {
105
+ const index = list.findIndex((n) => n.id === id);
106
+ if (index !== -1) {
107
+ return [windowId, list[index], index];
108
+ }
109
+ }
110
+ return void 0;
111
+ }
112
+ const descriptor = {
113
+ name: "notifications",
114
+ version: NOTIFICATION_SERVICE_VERSION,
115
+ description: "Notification state registry \u2014 tracks notifications per napplet window"
116
+ };
117
+ return {
118
+ descriptor,
119
+ handleMessage(windowId, message, send) {
120
+ const msg = message;
121
+ if (message.type.startsWith("notify.")) {
122
+ const action2 = message.type.slice(7);
123
+ switch (action2) {
124
+ case "create": {
125
+ const title = typeof msg.title === "string" ? msg.title : "";
126
+ const body = typeof msg.body === "string" ? msg.body : "";
127
+ const id = generateId();
128
+ const notification = {
129
+ id,
130
+ windowId,
131
+ title,
132
+ body,
133
+ read: false,
134
+ createdAt: Math.floor(Date.now() / 1e3)
135
+ };
136
+ const list = getWindowNotifications(windowId);
137
+ list.push(notification);
138
+ enforceLimit(list);
139
+ notify();
140
+ send({ type: "notify.created", id });
141
+ break;
142
+ }
143
+ case "dismiss": {
144
+ const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
145
+ if (!notifId) return;
146
+ const found = findById(notifId);
147
+ if (found) {
148
+ const [foundWindowId, , index] = found;
149
+ const list = notifications.get(foundWindowId);
150
+ if (list) {
151
+ list.splice(index, 1);
152
+ if (list.length === 0) notifications.delete(foundWindowId);
153
+ notify();
154
+ }
155
+ }
156
+ break;
157
+ }
158
+ case "read": {
159
+ const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
160
+ if (!notifId) return;
161
+ const found = findById(notifId);
162
+ if (found) {
163
+ const [, notification] = found;
164
+ if (!notification.read) {
165
+ notification.read = true;
166
+ notify();
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ case "list": {
172
+ const windowNotifs = notifications.get(windowId) ?? [];
173
+ send({ type: "notify.listed", notifications: windowNotifs });
174
+ break;
175
+ }
176
+ default:
177
+ break;
178
+ }
179
+ return;
180
+ }
181
+ if (message.type !== "ifc.emit") return;
182
+ const topic = msg.topic;
183
+ if (!topic?.startsWith("notifications:")) return;
184
+ const action = topic.slice(14);
185
+ const payload = msg.payload ?? {};
186
+ switch (action) {
187
+ case "create": {
188
+ const title = typeof payload.title === "string" ? payload.title : "";
189
+ const body = typeof payload.body === "string" ? payload.body : "";
190
+ const id = generateId();
191
+ const notification = {
192
+ id,
193
+ windowId,
194
+ title,
195
+ body,
196
+ read: false,
197
+ createdAt: Math.floor(Date.now() / 1e3)
198
+ };
199
+ const list = getWindowNotifications(windowId);
200
+ list.push(notification);
201
+ enforceLimit(list);
202
+ notify();
203
+ send({ type: "ifc.event", topic: "notifications:created", payload: { id } });
204
+ break;
205
+ }
206
+ case "dismiss": {
207
+ const id = typeof payload.id === "string" ? payload.id : "";
208
+ if (!id) return;
209
+ const found = findById(id);
210
+ if (found) {
211
+ const [foundWindowId, , index] = found;
212
+ const list = notifications.get(foundWindowId);
213
+ if (list) {
214
+ list.splice(index, 1);
215
+ if (list.length === 0) {
216
+ notifications.delete(foundWindowId);
217
+ }
218
+ notify();
219
+ }
220
+ }
221
+ break;
222
+ }
223
+ case "read": {
224
+ const id = typeof payload.id === "string" ? payload.id : "";
225
+ if (!id) return;
226
+ const found = findById(id);
227
+ if (found) {
228
+ const [, notification] = found;
229
+ if (!notification.read) {
230
+ notification.read = true;
231
+ notify();
232
+ }
233
+ }
234
+ break;
235
+ }
236
+ case "list": {
237
+ const windowNotifs = notifications.get(windowId) ?? [];
238
+ send({ type: "ifc.event", topic: "notifications:listed", payload: { notifications: windowNotifs } });
239
+ break;
240
+ }
241
+ default:
242
+ break;
243
+ }
244
+ },
245
+ onWindowDestroyed(windowId) {
246
+ if (notifications.delete(windowId)) {
247
+ notify();
248
+ }
249
+ }
250
+ };
251
+ }
252
+
253
+ // src/identity-service.ts
254
+ var IDENTITY_SERVICE_VERSION = "1.0.0";
255
+ function createIdentityService(options) {
256
+ return {
257
+ descriptor: {
258
+ name: "identity",
259
+ version: IDENTITY_SERVICE_VERSION,
260
+ description: "NIP-5D identity NUB reference handler (9 read-only identity queries)"
261
+ },
262
+ handleMessage(_windowId, message, send) {
263
+ const id = message.id ?? "";
264
+ function sendError(typeBase, error) {
265
+ send({ type: `${typeBase}.error`, id, error });
266
+ }
267
+ const signer = options.getSigner();
268
+ switch (message.type) {
269
+ case "identity.getPublicKey": {
270
+ if (!signer) {
271
+ const result = {
272
+ type: "identity.getPublicKey.result",
273
+ id,
274
+ pubkey: ""
275
+ };
276
+ send(result);
277
+ return;
278
+ }
279
+ Promise.resolve(signer.getPublicKey?.()).then((pubkey) => {
280
+ const result = {
281
+ type: "identity.getPublicKey.result",
282
+ id,
283
+ pubkey: pubkey ?? ""
284
+ };
285
+ send(result);
286
+ }).catch((err) => {
287
+ sendError(
288
+ "identity.getPublicKey",
289
+ err?.message ?? "getPublicKey failed"
290
+ );
291
+ });
292
+ return;
293
+ }
294
+ case "identity.getRelays": {
295
+ if (!signer) {
296
+ sendError("identity.getRelays", "no signer configured");
297
+ return;
298
+ }
299
+ Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => {
300
+ const result = {
301
+ type: "identity.getRelays.result",
302
+ id,
303
+ relays
304
+ };
305
+ send(result);
306
+ }).catch((err) => {
307
+ sendError(
308
+ "identity.getRelays",
309
+ err?.message ?? "getRelays failed"
310
+ );
311
+ });
312
+ return;
313
+ }
314
+ case "identity.getProfile": {
315
+ const result = {
316
+ type: "identity.getProfile.result",
317
+ id,
318
+ profile: null
319
+ };
320
+ send(result);
321
+ return;
322
+ }
323
+ case "identity.getFollows": {
324
+ const result = {
325
+ type: "identity.getFollows.result",
326
+ id,
327
+ pubkeys: []
328
+ };
329
+ send(result);
330
+ return;
331
+ }
332
+ case "identity.getList": {
333
+ const result = {
334
+ type: "identity.getList.result",
335
+ id,
336
+ entries: []
337
+ };
338
+ send(result);
339
+ return;
340
+ }
341
+ case "identity.getZaps": {
342
+ const result = {
343
+ type: "identity.getZaps.result",
344
+ id,
345
+ zaps: []
346
+ };
347
+ send(result);
348
+ return;
349
+ }
350
+ case "identity.getMutes": {
351
+ const result = {
352
+ type: "identity.getMutes.result",
353
+ id,
354
+ pubkeys: []
355
+ };
356
+ send(result);
357
+ return;
358
+ }
359
+ case "identity.getBlocked": {
360
+ const result = {
361
+ type: "identity.getBlocked.result",
362
+ id,
363
+ pubkeys: []
364
+ };
365
+ send(result);
366
+ return;
367
+ }
368
+ case "identity.getBadges": {
369
+ const result = {
370
+ type: "identity.getBadges.result",
371
+ id,
372
+ badges: []
373
+ };
374
+ send(result);
375
+ return;
376
+ }
377
+ default:
378
+ sendError(message.type, `Unknown identity method: ${message.type}`);
379
+ }
380
+ },
381
+ // Identity service has no per-window state to clean up.
382
+ onWindowDestroyed(_windowId) {
383
+ }
384
+ };
385
+ }
386
+
387
+ // src/relay-pool-service.ts
388
+ var EOSE_FALLBACK_MS = 15e3;
389
+ function createRelayPoolService(options) {
390
+ const tracked = /* @__PURE__ */ new Map();
391
+ return {
392
+ descriptor: {
393
+ name: "relay-pool",
394
+ version: "1.0.0",
395
+ description: "Relay pool subscription and publishing"
396
+ },
397
+ handleMessage(windowId, message, send) {
398
+ if (message.type === "relay.subscribe") {
399
+ const subId = message.subId;
400
+ if (typeof subId !== "string") return;
401
+ const filters = message.filters;
402
+ const subKey = `${windowId}:${subId}`;
403
+ const existing = tracked.get(subKey);
404
+ if (existing) {
405
+ existing.handle.unsubscribe();
406
+ clearTimeout(existing.eoseTimer);
407
+ tracked.delete(subKey);
408
+ }
409
+ if (!options.isAvailable()) {
410
+ send({ type: "relay.eose", subId });
411
+ return;
412
+ }
413
+ const relayUrls = options.selectRelayTier(filters);
414
+ let eoseSent = false;
415
+ const eoseTimer = setTimeout(() => {
416
+ if (!eoseSent) {
417
+ eoseSent = true;
418
+ send({ type: "relay.eose", subId });
419
+ }
420
+ }, EOSE_FALLBACK_MS);
421
+ const handle = options.subscribe(filters, (item) => {
422
+ if (item === "EOSE") {
423
+ clearTimeout(eoseTimer);
424
+ if (!eoseSent) {
425
+ eoseSent = true;
426
+ send({ type: "relay.eose", subId });
427
+ }
428
+ return;
429
+ }
430
+ send({ type: "relay.event", subId, event: item });
431
+ }, relayUrls);
432
+ tracked.set(subKey, { handle, eoseTimer });
433
+ return;
434
+ }
435
+ if (message.type === "relay.close") {
436
+ const subId = message.subId;
437
+ if (typeof subId !== "string") return;
438
+ const subKey = `${windowId}:${subId}`;
439
+ const entry = tracked.get(subKey);
440
+ if (entry) {
441
+ entry.handle.unsubscribe();
442
+ clearTimeout(entry.eoseTimer);
443
+ tracked.delete(subKey);
444
+ }
445
+ return;
446
+ }
447
+ if (message.type === "relay.publish") {
448
+ const event = message.event;
449
+ if (event && typeof event === "object" && options.isAvailable()) {
450
+ options.publish(event);
451
+ }
452
+ return;
453
+ }
454
+ if (message.type === "relay.publishEncrypted") {
455
+ const event = message.event;
456
+ if (event && typeof event === "object" && options.isAvailable()) {
457
+ options.publish(event);
458
+ }
459
+ return;
460
+ }
461
+ },
462
+ onWindowDestroyed(windowId) {
463
+ const prefix = `${windowId}:`;
464
+ for (const [key, entry] of tracked) {
465
+ if (key.startsWith(prefix)) {
466
+ entry.handle.unsubscribe();
467
+ clearTimeout(entry.eoseTimer);
468
+ tracked.delete(key);
469
+ }
470
+ }
471
+ }
472
+ };
473
+ }
474
+
475
+ // src/cache-service.ts
476
+ function createCacheService(options) {
477
+ return {
478
+ descriptor: {
479
+ name: "cache",
480
+ version: "1.0.0",
481
+ description: "Local event cache (IndexedDB, worker relay, etc.)"
482
+ },
483
+ handleMessage(_windowId, message, send) {
484
+ if (message.type === "relay.subscribe") {
485
+ const subId = message.subId;
486
+ if (typeof subId !== "string") return;
487
+ const filters = message.filters;
488
+ if (!options.isAvailable()) {
489
+ send({ type: "relay.eose", subId });
490
+ return;
491
+ }
492
+ options.query(filters).then((events) => {
493
+ for (const event of events) {
494
+ send({ type: "relay.event", subId, event });
495
+ }
496
+ send({ type: "relay.eose", subId });
497
+ }).catch(() => {
498
+ send({ type: "relay.eose", subId });
499
+ });
500
+ return;
501
+ }
502
+ if (message.type === "relay.publish") {
503
+ const event = message.event;
504
+ if (event && typeof event === "object" && options.isAvailable()) {
505
+ try {
506
+ options.store(event);
507
+ } catch {
508
+ }
509
+ }
510
+ return;
511
+ }
512
+ },
513
+ // Cache has no per-window state to clean up
514
+ onWindowDestroyed(_windowId) {
515
+ }
516
+ };
517
+ }
518
+
519
+ // src/coordinated-relay.ts
520
+ var DEFAULT_EOSE_TIMEOUT_MS = 15e3;
521
+ function createCoordinatedRelay(options) {
522
+ const timeoutMs = options.eoseTimeoutMs ?? DEFAULT_EOSE_TIMEOUT_MS;
523
+ const subs = /* @__PURE__ */ new Map();
524
+ function maybeSendEose(subKey, subId, send) {
525
+ const sub = subs.get(subKey);
526
+ if (!sub || sub.eoseSent) return;
527
+ if (sub.cacheEose && sub.relayEose) {
528
+ sub.eoseSent = true;
529
+ clearTimeout(sub.eoseTimer);
530
+ send({ type: "relay.eose", subId });
531
+ }
532
+ }
533
+ return {
534
+ descriptor: {
535
+ name: "relay",
536
+ version: "1.0.0",
537
+ description: "Coordinated relay pool + cache with dedup and unified EOSE"
538
+ },
539
+ handleMessage(windowId, message, send) {
540
+ if (message.type === "relay.subscribe") {
541
+ let deliver2 = function(event) {
542
+ if (tracked.seenIds.has(event.id)) return;
543
+ tracked.seenIds.add(event.id);
544
+ if (subs.has(subKey)) send({ type: "relay.event", subId, event });
545
+ };
546
+ var deliver = deliver2;
547
+ const subId = message.subId;
548
+ if (typeof subId !== "string") return;
549
+ const filters = message.filters;
550
+ const subKey = `${windowId}:${subId}`;
551
+ const existing = subs.get(subKey);
552
+ if (existing) {
553
+ existing.relayHandle?.unsubscribe();
554
+ clearTimeout(existing.eoseTimer);
555
+ subs.delete(subKey);
556
+ }
557
+ const cacheAvailable = options.cache.isAvailable();
558
+ const relayAvailable = options.relayPool.isAvailable();
559
+ if (!cacheAvailable && !relayAvailable) {
560
+ send({ type: "relay.eose", subId });
561
+ return;
562
+ }
563
+ const tracked = {
564
+ seenIds: /* @__PURE__ */ new Set(),
565
+ cacheEose: !cacheAvailable,
566
+ // mark done if cache not available
567
+ relayEose: !relayAvailable,
568
+ // mark done if relay not available
569
+ eoseSent: false,
570
+ eoseTimer: null,
571
+ relayHandle: null
572
+ };
573
+ subs.set(subKey, tracked);
574
+ if (cacheAvailable) {
575
+ options.cache.query(filters).then((events) => {
576
+ for (const event of events) deliver2(event);
577
+ tracked.cacheEose = true;
578
+ maybeSendEose(subKey, subId, send);
579
+ }).catch(() => {
580
+ tracked.cacheEose = true;
581
+ maybeSendEose(subKey, subId, send);
582
+ });
583
+ }
584
+ if (relayAvailable) {
585
+ tracked.eoseTimer = setTimeout(() => {
586
+ if (!tracked.eoseSent) {
587
+ tracked.relayEose = true;
588
+ maybeSendEose(subKey, subId, send);
589
+ }
590
+ }, timeoutMs);
591
+ const relayUrls = options.relayPool.selectRelayTier(filters);
592
+ tracked.relayHandle = options.relayPool.subscribe(filters, (item) => {
593
+ if (item === "EOSE") {
594
+ clearTimeout(tracked.eoseTimer);
595
+ tracked.relayEose = true;
596
+ maybeSendEose(subKey, subId, send);
597
+ return;
598
+ }
599
+ deliver2(item);
600
+ if (cacheAvailable) {
601
+ try {
602
+ options.cache.store(item);
603
+ } catch {
604
+ }
605
+ }
606
+ }, relayUrls);
607
+ }
608
+ return;
609
+ }
610
+ if (message.type === "relay.close") {
611
+ const subId = message.subId;
612
+ if (typeof subId !== "string") return;
613
+ const subKey = `${windowId}:${subId}`;
614
+ const entry = subs.get(subKey);
615
+ if (entry) {
616
+ entry.relayHandle?.unsubscribe();
617
+ clearTimeout(entry.eoseTimer);
618
+ subs.delete(subKey);
619
+ }
620
+ return;
621
+ }
622
+ if (message.type === "relay.publish") {
623
+ const event = message.event;
624
+ if (!event || typeof event !== "object") return;
625
+ if (options.relayPool.isAvailable()) {
626
+ options.relayPool.publish(event);
627
+ }
628
+ if (options.cache.isAvailable()) {
629
+ try {
630
+ options.cache.store(event);
631
+ } catch {
632
+ }
633
+ }
634
+ return;
635
+ }
636
+ },
637
+ onWindowDestroyed(windowId) {
638
+ const prefix = `${windowId}:`;
639
+ for (const [key, entry] of subs) {
640
+ if (key.startsWith(prefix)) {
641
+ entry.relayHandle?.unsubscribe();
642
+ clearTimeout(entry.eoseTimer);
643
+ subs.delete(key);
644
+ }
645
+ }
646
+ }
647
+ };
648
+ }
649
+
650
+ // src/keys-service.ts
651
+ var KEYS_SERVICE_VERSION = "1.0.0";
652
+ function createKeysService(options = {}) {
653
+ const descriptor = {
654
+ name: "keys",
655
+ version: KEYS_SERVICE_VERSION,
656
+ description: "NIP-5D keys NUB reference handler (stub)"
657
+ };
658
+ return {
659
+ descriptor,
660
+ handleMessage(_windowId, message, send) {
661
+ switch (message.type) {
662
+ case "keys.forward": {
663
+ const m = message;
664
+ options.onForward?.({
665
+ key: m.key,
666
+ code: m.code,
667
+ ctrlKey: m.ctrl,
668
+ altKey: m.alt,
669
+ shiftKey: m.shift,
670
+ metaKey: m.meta
671
+ });
672
+ return;
673
+ }
674
+ case "keys.registerAction": {
675
+ const m = message;
676
+ const result = {
677
+ type: "keys.registerAction.result",
678
+ id: m.id,
679
+ actionId: m.action.id,
680
+ ...m.action.defaultKey ? { binding: m.action.defaultKey } : {}
681
+ };
682
+ send(result);
683
+ return;
684
+ }
685
+ case "keys.unregisterAction": {
686
+ return;
687
+ }
688
+ default: {
689
+ const id = message.id ?? "";
690
+ send({
691
+ type: `${message.type}.error`,
692
+ id,
693
+ error: `Unknown keys method: ${message.type}`
694
+ });
695
+ return;
696
+ }
697
+ }
698
+ },
699
+ onWindowDestroyed(_windowId) {
700
+ }
701
+ };
702
+ }
703
+
704
+ // src/media-service.ts
705
+ var MEDIA_SERVICE_VERSION = "1.0.0";
706
+ function createMediaService(options = {}) {
707
+ const descriptor = {
708
+ name: "media",
709
+ version: MEDIA_SERVICE_VERSION,
710
+ description: "NIP-5D media NUB reference handler (stub)"
711
+ };
712
+ return {
713
+ descriptor,
714
+ handleMessage(windowId, message, send) {
715
+ switch (message.type) {
716
+ case "media.session.create": {
717
+ const m = message;
718
+ options.onSessionCreate?.(windowId, m.sessionId, m.metadata);
719
+ const result = {
720
+ type: "media.session.create.result",
721
+ id: m.id,
722
+ sessionId: m.sessionId
723
+ };
724
+ send(result);
725
+ return;
726
+ }
727
+ case "media.session.update": {
728
+ const m = message;
729
+ options.onSessionUpdate?.(windowId, m.sessionId ?? "", m.metadata);
730
+ return;
731
+ }
732
+ case "media.session.destroy": {
733
+ const m = message;
734
+ options.onSessionDestroy?.(windowId, m.sessionId ?? "");
735
+ return;
736
+ }
737
+ case "media.state": {
738
+ const m = message;
739
+ options.onState?.(windowId, m.sessionId ?? "", m);
740
+ return;
741
+ }
742
+ case "media.capabilities": {
743
+ const m = message;
744
+ options.onCapabilities?.(windowId, m.sessionId ?? "", m.actions);
745
+ return;
746
+ }
747
+ default: {
748
+ const id = message.id ?? "";
749
+ send({
750
+ type: `${message.type}.error`,
751
+ id,
752
+ error: `Unknown media method: ${message.type}`
753
+ });
754
+ return;
755
+ }
756
+ }
757
+ },
758
+ onWindowDestroyed(_windowId) {
759
+ }
760
+ };
761
+ }
762
+
763
+ // src/notify-service.ts
764
+ var NOTIFY_SERVICE_VERSION = "1.0.0";
765
+ function createNotifyService(options = {}) {
766
+ let counter = 0;
767
+ const gen = options.generateId ?? (() => {
768
+ counter += 1;
769
+ return `shell-${counter}`;
770
+ });
771
+ const defaultGrant = options.defaultGrant ?? true;
772
+ const descriptor = {
773
+ name: "notify",
774
+ version: NOTIFY_SERVICE_VERSION,
775
+ description: "NIP-5D notify NUB reference handler (stub)"
776
+ };
777
+ return {
778
+ descriptor,
779
+ handleMessage(windowId, message, send) {
780
+ switch (message.type) {
781
+ case "notify.send": {
782
+ const m = message;
783
+ options.onSend?.(windowId, m);
784
+ const result = {
785
+ type: "notify.send.result",
786
+ id: m.id,
787
+ notificationId: gen()
788
+ };
789
+ send(result);
790
+ return;
791
+ }
792
+ case "notify.dismiss":
793
+ case "notify.badge":
794
+ case "notify.channel.register":
795
+ return;
796
+ case "notify.permission.request": {
797
+ const m = message;
798
+ const result = {
799
+ type: "notify.permission.result",
800
+ id: m.id,
801
+ granted: defaultGrant
802
+ };
803
+ send(result);
804
+ return;
805
+ }
806
+ default: {
807
+ const id = message.id ?? "";
808
+ send({
809
+ type: `${message.type}.error`,
810
+ id,
811
+ error: `Unknown notify method: ${message.type}`
812
+ });
813
+ }
814
+ }
815
+ },
816
+ onWindowDestroyed(_windowId) {
817
+ }
818
+ };
819
+ }
820
+
821
+ // src/theme-service.ts
822
+ var THEME_SERVICE_VERSION = "1.0.0";
823
+ var DEFAULT_THEME = {
824
+ colors: {
825
+ background: "#0a0a0a",
826
+ text: "#e0e0e0",
827
+ primary: "#7aa2f7"
828
+ }
829
+ // fonts, background, title intentionally undefined — all optional per
830
+ // @napplet/nub-theme Theme interface.
831
+ };
832
+ function createThemeService(options = {}) {
833
+ let currentTheme = options.initialTheme ?? DEFAULT_THEME;
834
+ const descriptor = {
835
+ name: "theme",
836
+ version: THEME_SERVICE_VERSION,
837
+ description: "NIP-5D theme NUB reference handler"
838
+ };
839
+ const handler = {
840
+ descriptor,
841
+ handleMessage(_windowId, message, send) {
842
+ const id = message.id ?? "";
843
+ if (message.type === "theme.get") {
844
+ const result = {
845
+ type: "theme.get.result",
846
+ id,
847
+ theme: currentTheme
848
+ };
849
+ send(result);
850
+ return;
851
+ }
852
+ send({
853
+ type: `${message.type}.error`,
854
+ id,
855
+ error: `Unknown theme method: ${message.type}`
856
+ });
857
+ },
858
+ // Theme service has no per-window state to clean up.
859
+ onWindowDestroyed(_windowId) {
860
+ }
861
+ };
862
+ function publishTheme(theme) {
863
+ currentTheme = theme;
864
+ const envelope = { type: "theme.changed", theme };
865
+ options.onBroadcast?.(envelope);
866
+ return envelope;
867
+ }
868
+ function getCurrentTheme() {
869
+ return currentTheme;
870
+ }
871
+ return { handler, publishTheme, getCurrentTheme };
872
+ }
873
+ export {
874
+ createAudioService,
875
+ createCacheService,
876
+ createCoordinatedRelay,
877
+ createIdentityService,
878
+ createKeysService,
879
+ createMediaService,
880
+ createNotificationService,
881
+ createNotifyService,
882
+ createRelayPoolService,
883
+ createThemeService
884
+ };
885
+ //# sourceMappingURL=index.js.map