@kehto/shell 0.1.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,1129 @@
1
+ // src/shell-bridge.ts
2
+ import { createRuntime } from "@kehto/runtime";
3
+
4
+ // src/hooks-adapter.ts
5
+ function bytesToHex(bytes) {
6
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
7
+ }
8
+ function hexToBytes(hex) {
9
+ const bytes = new Uint8Array(hex.length / 2);
10
+ for (let i = 0; i < hex.length; i += 2) {
11
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
12
+ }
13
+ return bytes;
14
+ }
15
+ function adaptHooks(shellHooks, deps) {
16
+ const { originRegistry: originRegistry2 } = deps;
17
+ const sendToNapplet = (windowId, msg) => {
18
+ const win = originRegistry2.getIframeWindow(windowId);
19
+ if (win) win.postMessage(msg, "*");
20
+ };
21
+ const relayPool = {
22
+ subscribe(filters, callback, relayUrls) {
23
+ const pool = shellHooks.relayPool.getRelayPool();
24
+ if (!pool) return { unsubscribe() {
25
+ } };
26
+ const urls = relayUrls ?? shellHooks.relayPool.selectRelayTier(filters);
27
+ const sub = pool.subscription(urls, filters).subscribe((item) => {
28
+ if (item === "EOSE") {
29
+ callback("EOSE");
30
+ } else {
31
+ callback(item);
32
+ }
33
+ });
34
+ return { unsubscribe: () => sub.unsubscribe() };
35
+ },
36
+ publish(event) {
37
+ const pool = shellHooks.relayPool.getRelayPool();
38
+ if (!pool) return;
39
+ const relayUrls = shellHooks.relayPool.selectRelayTier([]);
40
+ pool.publish(relayUrls, event);
41
+ },
42
+ selectRelayTier(filters) {
43
+ return shellHooks.relayPool.selectRelayTier(filters);
44
+ },
45
+ trackSubscription(subKey, cleanup) {
46
+ shellHooks.relayPool.trackSubscription(subKey, cleanup);
47
+ },
48
+ untrackSubscription(subKey) {
49
+ shellHooks.relayPool.untrackSubscription(subKey);
50
+ },
51
+ openScopedRelay(windowId, relayUrl, subId, filters, sendFn) {
52
+ const win = originRegistry2.getIframeWindow(windowId);
53
+ if (win) shellHooks.relayPool.openScopedRelay(windowId, relayUrl, subId, filters, win);
54
+ },
55
+ closeScopedRelay(windowId) {
56
+ shellHooks.relayPool.closeScopedRelay(windowId);
57
+ },
58
+ publishToScopedRelay(windowId, event) {
59
+ return shellHooks.relayPool.publishToScopedRelay(windowId, event);
60
+ },
61
+ isAvailable() {
62
+ return shellHooks.relayPool.getRelayPool() !== null;
63
+ }
64
+ };
65
+ const cache2 = {
66
+ async query(filters) {
67
+ const workerRelay = shellHooks.workerRelay.getWorkerRelay();
68
+ if (!workerRelay) return [];
69
+ const subId = crypto.randomUUID();
70
+ return workerRelay.query(["REQ", subId, ...filters]);
71
+ },
72
+ store(event) {
73
+ const workerRelay = shellHooks.workerRelay.getWorkerRelay();
74
+ if (!workerRelay) return;
75
+ try {
76
+ workerRelay.event(event)?.catch?.(() => {
77
+ });
78
+ } catch {
79
+ }
80
+ },
81
+ isAvailable() {
82
+ return shellHooks.workerRelay.getWorkerRelay() !== null;
83
+ }
84
+ };
85
+ const auth = {
86
+ getUserPubkey() {
87
+ return shellHooks.auth.getUserPubkey();
88
+ },
89
+ getSigner() {
90
+ return shellHooks.auth.getSigner();
91
+ }
92
+ };
93
+ const config = {
94
+ getNappUpdateBehavior() {
95
+ return shellHooks.config.getNappUpdateBehavior();
96
+ }
97
+ };
98
+ const hotkeys = {
99
+ executeHotkeyFromForward(event) {
100
+ shellHooks.hotkeys.executeHotkeyFromForward(event);
101
+ }
102
+ };
103
+ const cryptoHooks = {
104
+ async verifyEvent(event) {
105
+ return shellHooks.crypto.verifyEvent(event);
106
+ },
107
+ randomUUID() {
108
+ return crypto.randomUUID();
109
+ },
110
+ randomBytes(length) {
111
+ const bytes = new Uint8Array(length);
112
+ crypto.getRandomValues(bytes);
113
+ return bytes;
114
+ }
115
+ };
116
+ const aclPersistence = {
117
+ persist(data) {
118
+ try {
119
+ localStorage.setItem("napplet:acl", data);
120
+ } catch {
121
+ }
122
+ },
123
+ load() {
124
+ try {
125
+ return localStorage.getItem("napplet:acl");
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ };
131
+ const manifestPersistence = {
132
+ persist(data) {
133
+ try {
134
+ localStorage.setItem("napplet:manifest-cache", data);
135
+ } catch {
136
+ }
137
+ },
138
+ load() {
139
+ try {
140
+ return localStorage.getItem("napplet:manifest-cache");
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ };
146
+ const statePersistence = {
147
+ get(scopedKey) {
148
+ try {
149
+ return localStorage.getItem(scopedKey);
150
+ } catch {
151
+ return null;
152
+ }
153
+ },
154
+ set(scopedKey, value) {
155
+ try {
156
+ localStorage.setItem(scopedKey, value);
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ },
162
+ remove(scopedKey) {
163
+ try {
164
+ localStorage.removeItem(scopedKey);
165
+ } catch {
166
+ }
167
+ },
168
+ clear(prefix) {
169
+ try {
170
+ const keysToRemove = [];
171
+ for (let i = 0; i < localStorage.length; i++) {
172
+ const key = localStorage.key(i);
173
+ if (key?.startsWith(prefix)) keysToRemove.push(key);
174
+ }
175
+ for (const key of keysToRemove) localStorage.removeItem(key);
176
+ } catch {
177
+ }
178
+ },
179
+ keys(prefix) {
180
+ try {
181
+ const result = [];
182
+ for (let i = 0; i < localStorage.length; i++) {
183
+ const key = localStorage.key(i);
184
+ if (key?.startsWith(prefix)) result.push(key);
185
+ }
186
+ return result;
187
+ } catch {
188
+ return [];
189
+ }
190
+ },
191
+ calculateBytes(prefix, excludeKey) {
192
+ try {
193
+ let total = 0;
194
+ for (let i = 0; i < localStorage.length; i++) {
195
+ const key = localStorage.key(i);
196
+ if (!key?.startsWith(prefix)) continue;
197
+ if (excludeKey && key === excludeKey) continue;
198
+ const value = localStorage.getItem(key) ?? "";
199
+ total += new TextEncoder().encode(key + value).length;
200
+ }
201
+ return total;
202
+ } catch {
203
+ return 0;
204
+ }
205
+ }
206
+ };
207
+ const windowManager = {
208
+ createWindow(options) {
209
+ return shellHooks.windowManager.createWindow(options);
210
+ }
211
+ };
212
+ const relayConfig = {
213
+ addRelay(tier, url) {
214
+ shellHooks.relayConfig.addRelay(tier, url);
215
+ },
216
+ removeRelay(tier, url) {
217
+ shellHooks.relayConfig.removeRelay(tier, url);
218
+ },
219
+ getRelayConfig() {
220
+ return shellHooks.relayConfig.getRelayConfig();
221
+ },
222
+ getNip66Suggestions() {
223
+ return shellHooks.relayConfig.getNip66Suggestions();
224
+ }
225
+ };
226
+ const shellSecretPersistence = {
227
+ get() {
228
+ try {
229
+ const hex = localStorage.getItem("napplet-shell-secret");
230
+ if (!hex) return null;
231
+ return hexToBytes(hex);
232
+ } catch {
233
+ return null;
234
+ }
235
+ },
236
+ set(secret) {
237
+ try {
238
+ localStorage.setItem("napplet-shell-secret", bytesToHex(secret));
239
+ } catch {
240
+ }
241
+ }
242
+ };
243
+ const guidPersistence = {
244
+ get(windowId) {
245
+ try {
246
+ return localStorage.getItem(`napplet-guid:${windowId}`);
247
+ } catch {
248
+ return null;
249
+ }
250
+ },
251
+ set(windowId, guid) {
252
+ try {
253
+ localStorage.setItem(`napplet-guid:${windowId}`, guid);
254
+ } catch {
255
+ }
256
+ },
257
+ remove(windowId) {
258
+ try {
259
+ localStorage.removeItem(`napplet-guid:${windowId}`);
260
+ } catch {
261
+ }
262
+ }
263
+ };
264
+ const dm = shellHooks.dm ? {
265
+ sendDm(recipientPubkey, message) {
266
+ return shellHooks.dm.sendDm(recipientPubkey, message);
267
+ }
268
+ } : void 0;
269
+ return {
270
+ sendToNapplet,
271
+ relayPool,
272
+ cache: cache2,
273
+ auth,
274
+ config,
275
+ hotkeys,
276
+ crypto: cryptoHooks,
277
+ aclPersistence,
278
+ manifestPersistence,
279
+ statePersistence,
280
+ windowManager,
281
+ relayConfig,
282
+ dm,
283
+ shellSecretPersistence,
284
+ guidPersistence,
285
+ onAclCheck: shellHooks.onAclCheck,
286
+ onHashMismatch: shellHooks.onHashMismatch,
287
+ services: shellHooks.services,
288
+ getConfigOverrides: shellHooks.getConfigOverrides
289
+ };
290
+ }
291
+
292
+ // src/origin-registry.ts
293
+ var registry = /* @__PURE__ */ new Map();
294
+ var originRegistry = {
295
+ /**
296
+ * Register a window reference with a windowId and optional identity metadata.
297
+ *
298
+ * @param win - The iframe's contentWindow reference
299
+ * @param windowId - The unique identifier for this napplet window
300
+ * @param identity - Optional NIP-5D identity metadata (dTag and aggregateHash)
301
+ */
302
+ register(win, windowId, identity) {
303
+ registry.set(win, {
304
+ windowId,
305
+ dTag: identity?.dTag,
306
+ aggregateHash: identity?.aggregateHash
307
+ });
308
+ },
309
+ /**
310
+ * Unregister a window by its windowId, removing the mapping.
311
+ *
312
+ * @param windowId - The window identifier to remove
313
+ */
314
+ unregister(windowId) {
315
+ for (const [win, entry] of registry.entries()) {
316
+ if (entry.windowId === windowId) {
317
+ registry.delete(win);
318
+ }
319
+ }
320
+ },
321
+ /**
322
+ * Look up the windowId for a given Window reference.
323
+ *
324
+ * @param win - The Window reference (typically from event.source)
325
+ * @returns The windowId string, or undefined if not registered
326
+ */
327
+ getWindowId(win) {
328
+ return registry.get(win)?.windowId;
329
+ },
330
+ /**
331
+ * Look up the Window reference for a given windowId.
332
+ *
333
+ * @param windowId - The window identifier to look up
334
+ * @returns The Window reference, or null if not found
335
+ */
336
+ getIframeWindow(windowId) {
337
+ for (const [win, entry] of registry.entries()) {
338
+ if (entry.windowId === windowId) return win;
339
+ }
340
+ return null;
341
+ },
342
+ /**
343
+ * Get all registered windowId strings.
344
+ *
345
+ * @returns Array of all registered window identifiers
346
+ */
347
+ getAllWindowIds() {
348
+ return Array.from(registry.values()).map((entry) => entry.windowId);
349
+ },
350
+ /**
351
+ * Get identity metadata for a registered Window.
352
+ *
353
+ * @param win - The Window reference to look up
354
+ * @returns Identity metadata, or undefined if not registered or no identity set
355
+ */
356
+ getIdentity(win) {
357
+ const entry = registry.get(win);
358
+ if (!entry?.dTag || !entry?.aggregateHash) return void 0;
359
+ return { dTag: entry.dTag, aggregateHash: entry.aggregateHash };
360
+ },
361
+ /** Clear all registrations. */
362
+ clear() {
363
+ registry.clear();
364
+ }
365
+ };
366
+
367
+ // src/session-registry.ts
368
+ var byWindowId = /* @__PURE__ */ new Map();
369
+ var byPubkey = /* @__PURE__ */ new Map();
370
+ var pendingUpdates = /* @__PURE__ */ new Map();
371
+ var _pendingVersion = 0;
372
+ var sessionRegistry = {
373
+ /**
374
+ * Register a napplet entry, mapping windowId to pubkey and vice versa.
375
+ *
376
+ * @param windowId - The window identifier
377
+ * @param entry - The verified napplet session entry from AUTH handshake
378
+ */
379
+ register(windowId, entry) {
380
+ byWindowId.set(windowId, entry.pubkey);
381
+ byPubkey.set(entry.pubkey, entry);
382
+ },
383
+ /**
384
+ * Unregister a napplet by windowId, removing both mappings.
385
+ *
386
+ * @param windowId - The window identifier to remove
387
+ */
388
+ unregister(windowId) {
389
+ const pubkey = byWindowId.get(windowId);
390
+ if (pubkey) {
391
+ byPubkey.delete(pubkey);
392
+ byWindowId.delete(windowId);
393
+ }
394
+ pendingUpdates.delete(windowId);
395
+ },
396
+ /**
397
+ * Get the pubkey associated with a windowId.
398
+ *
399
+ * @param windowId - The window identifier
400
+ * @returns The napplet's pubkey, or undefined if not registered
401
+ */
402
+ getPubkey(windowId) {
403
+ return byWindowId.get(windowId);
404
+ },
405
+ /**
406
+ * Get the full entry for a napplet pubkey.
407
+ *
408
+ * @param pubkey - The napplet's pubkey
409
+ * @returns The full SessionEntry, or undefined if not found
410
+ */
411
+ getEntry(pubkey) {
412
+ return byPubkey.get(pubkey);
413
+ },
414
+ /**
415
+ * Get the windowId for a napplet pubkey.
416
+ *
417
+ * @param pubkey - The napplet's pubkey
418
+ * @returns The windowId, or undefined if not found
419
+ */
420
+ getWindowId(pubkey) {
421
+ return byPubkey.get(pubkey)?.windowId;
422
+ },
423
+ /**
424
+ * Check if a windowId has a registered napplet.
425
+ *
426
+ * @param windowId - The window identifier
427
+ * @returns True if the windowId has a registered napplet
428
+ */
429
+ isRegistered(windowId) {
430
+ return byWindowId.has(windowId);
431
+ },
432
+ /**
433
+ * Get all registered napplet entries.
434
+ *
435
+ * @returns Array of all SessionEntry objects
436
+ */
437
+ getAllEntries() {
438
+ return Array.from(byPubkey.values());
439
+ },
440
+ /**
441
+ * Set a pending update for a window (napplet reconnected with different hash).
442
+ *
443
+ * @param windowId - The window identifier
444
+ * @param update - The pending update details with resolve callback
445
+ */
446
+ setPendingUpdate(windowId, update) {
447
+ pendingUpdates.set(windowId, update);
448
+ _pendingVersion++;
449
+ if (typeof window !== "undefined") {
450
+ window.dispatchEvent(new CustomEvent("napplet:pending-update", { detail: { windowId } }));
451
+ }
452
+ },
453
+ /**
454
+ * Get a pending update for a window.
455
+ *
456
+ * @param windowId - The window identifier
457
+ * @returns The pending update, or undefined if none
458
+ */
459
+ getPendingUpdate(windowId) {
460
+ return pendingUpdates.get(windowId);
461
+ },
462
+ /**
463
+ * Clear a pending update for a window.
464
+ *
465
+ * @param windowId - The window identifier
466
+ */
467
+ clearPendingUpdate(windowId) {
468
+ pendingUpdates.delete(windowId);
469
+ _pendingVersion++;
470
+ if (typeof window !== "undefined") {
471
+ window.dispatchEvent(new CustomEvent("napplet:pending-update", { detail: { windowId } }));
472
+ }
473
+ },
474
+ /** Clear all registrations and pending updates. */
475
+ clear() {
476
+ byWindowId.clear();
477
+ byPubkey.clear();
478
+ pendingUpdates.clear();
479
+ }
480
+ };
481
+ var nappKeyRegistry = sessionRegistry;
482
+
483
+ // src/acl-store.ts
484
+ import { ALL_CAPABILITIES } from "@kehto/runtime";
485
+ import { migrateAclState } from "@kehto/acl";
486
+ var STORAGE_KEY = "napplet:acl";
487
+ var DEFAULT_STATE_QUOTA = 512 * 1024;
488
+ function aclKey(_pubkey, dTag, aggregateHash) {
489
+ return `${dTag}:${aggregateHash}`;
490
+ }
491
+ var store = /* @__PURE__ */ new Map();
492
+ var CAP_BITS = {
493
+ "relay:read": 1,
494
+ "relay:write": 2,
495
+ "cache:read": 4,
496
+ "cache:write": 8,
497
+ "hotkey:forward": 16,
498
+ "identity:read": 32,
499
+ "keys:bind": 64,
500
+ "keys:forward": 128,
501
+ "state:read": 256,
502
+ "state:write": 512,
503
+ "media:control": 1024,
504
+ "notify:send": 2048,
505
+ "notify:channel": 4096,
506
+ "theme:read": 8192
507
+ };
508
+ function capArrayToBitfield(caps) {
509
+ let bits = 0;
510
+ for (const cap of caps) bits |= CAP_BITS[cap] ?? 0;
511
+ return bits;
512
+ }
513
+ function bitfieldToCapArray(bits) {
514
+ return Object.entries(CAP_BITS).filter(([, bit]) => (bits & bit) !== 0).map(([name]) => name);
515
+ }
516
+ function getOrCreate(pubkey, dTag, aggregateHash) {
517
+ const key = aclKey(pubkey, dTag, aggregateHash);
518
+ let entry = store.get(key);
519
+ if (!entry) {
520
+ entry = {
521
+ key,
522
+ pubkey,
523
+ dTag,
524
+ aggregateHash,
525
+ capabilities: new Set(ALL_CAPABILITIES),
526
+ blocked: false,
527
+ stateQuota: DEFAULT_STATE_QUOTA
528
+ };
529
+ store.set(key, entry);
530
+ }
531
+ return entry;
532
+ }
533
+ var aclStore = {
534
+ /**
535
+ * Check if a napp identity has a specific capability.
536
+ * Returns true for unknown identities (permissive default).
537
+ *
538
+ * @param pubkey - The napp's pubkey
539
+ * @param dTag - The napp's dTag
540
+ * @param aggregateHash - The napp's build hash
541
+ * @param capability - The capability to check
542
+ * @returns True if the capability is granted and the napp is not blocked
543
+ */
544
+ check(pubkey, dTag, aggregateHash, capability) {
545
+ const key = aclKey(pubkey, dTag, aggregateHash);
546
+ const entry = store.get(key);
547
+ if (!entry) return true;
548
+ if (entry.blocked) return false;
549
+ return entry.capabilities.has(capability);
550
+ },
551
+ /**
552
+ * Grant a capability to a napp identity.
553
+ *
554
+ * @param pubkey - The napp's pubkey
555
+ * @param dTag - The napp's dTag
556
+ * @param aggregateHash - The napp's build hash
557
+ * @param capability - The capability to grant
558
+ */
559
+ grant(pubkey, dTag, aggregateHash, capability) {
560
+ getOrCreate(pubkey, dTag, aggregateHash).capabilities.add(capability);
561
+ },
562
+ /**
563
+ * Revoke a capability from a napp identity.
564
+ *
565
+ * @param pubkey - The napp's pubkey
566
+ * @param dTag - The napp's dTag
567
+ * @param aggregateHash - The napp's build hash
568
+ * @param capability - The capability to revoke
569
+ */
570
+ revoke(pubkey, dTag, aggregateHash, capability) {
571
+ getOrCreate(pubkey, dTag, aggregateHash).capabilities.delete(capability);
572
+ },
573
+ /**
574
+ * Block a napp identity entirely (all capabilities denied).
575
+ *
576
+ * @param pubkey - The napp's pubkey
577
+ * @param dTag - The napp's dTag
578
+ * @param aggregateHash - The napp's build hash
579
+ */
580
+ block(pubkey, dTag, aggregateHash) {
581
+ getOrCreate(pubkey, dTag, aggregateHash).blocked = true;
582
+ },
583
+ /**
584
+ * Unblock a napp identity.
585
+ *
586
+ * @param pubkey - The napp's pubkey
587
+ * @param dTag - The napp's dTag
588
+ * @param aggregateHash - The napp's build hash
589
+ */
590
+ unblock(pubkey, dTag, aggregateHash) {
591
+ getOrCreate(pubkey, dTag, aggregateHash).blocked = false;
592
+ },
593
+ /**
594
+ * Check if a napp identity is blocked.
595
+ *
596
+ * @param pubkey - The napp's pubkey
597
+ * @param dTag - The napp's dTag
598
+ * @param aggregateHash - The napp's build hash
599
+ * @returns True if the identity is blocked
600
+ */
601
+ isBlocked(pubkey, dTag, aggregateHash) {
602
+ const key = aclKey(pubkey, dTag, aggregateHash);
603
+ return store.get(key)?.blocked ?? false;
604
+ },
605
+ /**
606
+ * Get the external ACL entry for a napp identity.
607
+ *
608
+ * @param pubkey - The napp's pubkey
609
+ * @param dTag - The napp's dTag
610
+ * @param aggregateHash - The napp's build hash
611
+ * @returns The ACL entry, or undefined if no explicit entry exists
612
+ */
613
+ getEntry(pubkey, dTag, aggregateHash) {
614
+ const key = aclKey(pubkey, dTag, aggregateHash);
615
+ const internal = store.get(key);
616
+ if (!internal) return void 0;
617
+ return {
618
+ pubkey: internal.pubkey,
619
+ capabilities: Array.from(internal.capabilities),
620
+ blocked: internal.blocked,
621
+ stateQuota: internal.stateQuota
622
+ };
623
+ },
624
+ /**
625
+ * Get all ACL entries.
626
+ *
627
+ * @returns Array of all ACL entries
628
+ */
629
+ getAllEntries() {
630
+ return Array.from(store.values()).map((e) => ({
631
+ pubkey: e.pubkey,
632
+ capabilities: Array.from(e.capabilities),
633
+ blocked: e.blocked,
634
+ stateQuota: e.stateQuota
635
+ }));
636
+ },
637
+ /** Persist the ACL store to localStorage. */
638
+ persist() {
639
+ try {
640
+ const entries = Array.from(store.entries()).map(([key, val]) => [
641
+ key,
642
+ {
643
+ pubkey: val.pubkey,
644
+ dTag: val.dTag,
645
+ aggregateHash: val.aggregateHash,
646
+ capabilities: Array.from(val.capabilities),
647
+ blocked: val.blocked,
648
+ stateQuota: val.stateQuota
649
+ }
650
+ ]);
651
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
652
+ } catch {
653
+ }
654
+ },
655
+ /** Load the ACL store from localStorage. Migrates old 3-segment keys to 2-segment format. */
656
+ load() {
657
+ try {
658
+ const raw = localStorage.getItem(STORAGE_KEY);
659
+ if (!raw) return;
660
+ let entries = JSON.parse(raw);
661
+ const hasOldKeys = entries.some(([key]) => key.split(":").length === 3);
662
+ if (hasOldKeys) {
663
+ const tempState = {
664
+ defaultPolicy: "permissive",
665
+ entries: Object.fromEntries(
666
+ entries.map(([key, val]) => [key, {
667
+ caps: capArrayToBitfield(val.capabilities),
668
+ blocked: val.blocked,
669
+ quota: val.stateQuota ?? DEFAULT_STATE_QUOTA
670
+ }])
671
+ )
672
+ };
673
+ const migrated = migrateAclState(tempState);
674
+ if (migrated !== tempState) {
675
+ entries = Object.entries(migrated.entries).map(([key, entry]) => {
676
+ const parts = key.split(":");
677
+ return [key, {
678
+ pubkey: "",
679
+ dTag: parts[0] ?? "",
680
+ aggregateHash: parts[1] ?? "",
681
+ capabilities: bitfieldToCapArray(entry.caps),
682
+ blocked: entry.blocked,
683
+ stateQuota: entry.quota
684
+ }];
685
+ });
686
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
687
+ }
688
+ }
689
+ store.clear();
690
+ for (const [key, val] of entries) {
691
+ if (val.dTag === void 0 || val.aggregateHash === void 0) continue;
692
+ store.set(key, {
693
+ key,
694
+ pubkey: val.pubkey,
695
+ dTag: val.dTag,
696
+ aggregateHash: val.aggregateHash,
697
+ capabilities: new Set(val.capabilities),
698
+ blocked: val.blocked,
699
+ stateQuota: val.stateQuota ?? DEFAULT_STATE_QUOTA
700
+ });
701
+ }
702
+ } catch {
703
+ store.clear();
704
+ }
705
+ },
706
+ /**
707
+ * Get the state quota for a napp identity.
708
+ *
709
+ * @param pubkey - The napp's pubkey
710
+ * @param dTag - The napp's dTag
711
+ * @param aggregateHash - The napp's build hash
712
+ * @returns The quota in bytes (defaults to DEFAULT_STATE_QUOTA)
713
+ */
714
+ getStateQuota(pubkey, dTag, aggregateHash) {
715
+ const key = aclKey(pubkey, dTag, aggregateHash);
716
+ return store.get(key)?.stateQuota ?? DEFAULT_STATE_QUOTA;
717
+ },
718
+ /** Clear all ACL entries and remove from localStorage. */
719
+ clear() {
720
+ store.clear();
721
+ try {
722
+ localStorage.removeItem(STORAGE_KEY);
723
+ } catch {
724
+ }
725
+ }
726
+ };
727
+
728
+ // src/manifest-cache.ts
729
+ var STORAGE_KEY2 = "napplet:manifest-cache";
730
+ var cache = /* @__PURE__ */ new Map();
731
+ function cacheKey(pubkey, dTag) {
732
+ return `${pubkey}:${dTag}`;
733
+ }
734
+ var manifestCache = {
735
+ /**
736
+ * Get a cached manifest entry by pubkey and dTag.
737
+ *
738
+ * @param pubkey - The napp's pubkey
739
+ * @param dTag - The napp's dTag
740
+ * @returns The cached entry, or undefined if not found
741
+ */
742
+ get(pubkey, dTag) {
743
+ return cache.get(cacheKey(pubkey, dTag));
744
+ },
745
+ /**
746
+ * Set (upsert) a manifest cache entry and persist to localStorage.
747
+ *
748
+ * @param entry - The manifest entry to cache
749
+ */
750
+ set(entry) {
751
+ cache.set(cacheKey(entry.pubkey, entry.dTag), entry);
752
+ manifestCache.persist();
753
+ },
754
+ /**
755
+ * Check if a specific hash is cached for a pubkey/dTag combination.
756
+ *
757
+ * @param pubkey - The napp's pubkey
758
+ * @param dTag - The napp's dTag
759
+ * @param hash - The aggregateHash to check
760
+ * @returns True if the exact hash matches the cached entry
761
+ */
762
+ has(pubkey, dTag, hash) {
763
+ const entry = cache.get(cacheKey(pubkey, dTag));
764
+ return !!entry && entry.aggregateHash === hash;
765
+ },
766
+ /**
767
+ * Remove a cached entry for a pubkey/dTag and persist.
768
+ *
769
+ * @param pubkey - The napp's pubkey
770
+ * @param dTag - The napp's dTag
771
+ */
772
+ remove(pubkey, dTag) {
773
+ cache.delete(cacheKey(pubkey, dTag));
774
+ manifestCache.persist();
775
+ },
776
+ /** Load the cache from localStorage. */
777
+ load() {
778
+ try {
779
+ const raw = localStorage.getItem(STORAGE_KEY2);
780
+ if (!raw) return;
781
+ const entries = JSON.parse(raw);
782
+ cache.clear();
783
+ for (const [key, val] of entries) cache.set(key, val);
784
+ } catch {
785
+ cache.clear();
786
+ }
787
+ },
788
+ /** Persist the cache to localStorage. */
789
+ persist() {
790
+ try {
791
+ localStorage.setItem(STORAGE_KEY2, JSON.stringify(Array.from(cache.entries())));
792
+ } catch {
793
+ }
794
+ },
795
+ /** Clear all cached entries and remove from localStorage. */
796
+ clear() {
797
+ cache.clear();
798
+ try {
799
+ localStorage.removeItem(STORAGE_KEY2);
800
+ } catch {
801
+ }
802
+ }
803
+ };
804
+
805
+ // src/audio-manager.ts
806
+ var sources = /* @__PURE__ */ new Map();
807
+ var version = 0;
808
+ function bump() {
809
+ version++;
810
+ if (typeof window !== "undefined") {
811
+ window.dispatchEvent(new CustomEvent("napplet:audio-changed"));
812
+ }
813
+ }
814
+ var audioManager = {
815
+ /**
816
+ * Register a new audio source for a window.
817
+ *
818
+ * @param windowId - The window identifier
819
+ * @param nappletClass - The napplet class/type (e.g., 'music-player')
820
+ * @param title - Human-readable title for the audio source
821
+ */
822
+ register(windowId, nappletClass, title) {
823
+ sources.set(windowId, { windowId, nappletClass, title, muted: false });
824
+ bump();
825
+ },
826
+ /**
827
+ * Unregister an audio source for a window.
828
+ *
829
+ * @param windowId - The window identifier to remove
830
+ */
831
+ unregister(windowId) {
832
+ if (sources.delete(windowId)) bump();
833
+ },
834
+ /**
835
+ * Update the state of an audio source (e.g., change title).
836
+ *
837
+ * @param windowId - The window identifier
838
+ * @param update - Partial update with optional title
839
+ */
840
+ updateState(windowId, update) {
841
+ const src = sources.get(windowId);
842
+ if (!src) return;
843
+ if (update.title !== void 0) src.title = update.title;
844
+ bump();
845
+ },
846
+ /**
847
+ * Mute or unmute an audio source and notify the napplet via postMessage.
848
+ *
849
+ * @param windowId - The window identifier
850
+ * @param muted - True to mute, false to unmute
851
+ * @example
852
+ * ```ts
853
+ * audioManager.mute('win-1', true); // mute
854
+ * audioManager.mute('win-1', false); // unmute
855
+ * ```
856
+ */
857
+ mute(windowId, muted) {
858
+ const src = sources.get(windowId);
859
+ if (src) {
860
+ src.muted = muted;
861
+ bump();
862
+ }
863
+ const iframeWindow = originRegistry.getIframeWindow(windowId);
864
+ if (iframeWindow) {
865
+ const muteEvent = {
866
+ kind: 29e3,
867
+ // IPC_PEER — inlined numeric after Phase 24 DRIFT-01 shim removal
868
+ created_at: Math.floor(Date.now() / 1e3),
869
+ tags: [["t", "napplet:audio-muted"]],
870
+ content: JSON.stringify({ muted }),
871
+ pubkey: "__shell__",
872
+ id: `audio-mute-${windowId}-${Date.now()}`,
873
+ sig: ""
874
+ };
875
+ iframeWindow.postMessage(["EVENT", "__shell__", muteEvent], "*");
876
+ }
877
+ },
878
+ /**
879
+ * Check if a window has a registered audio source.
880
+ *
881
+ * @param windowId - The window identifier
882
+ * @returns True if the window has an active audio source
883
+ */
884
+ has(windowId) {
885
+ return sources.has(windowId);
886
+ },
887
+ /**
888
+ * Get the audio source for a window.
889
+ *
890
+ * @param windowId - The window identifier
891
+ * @returns The AudioSource, or undefined if not found
892
+ */
893
+ get(windowId) {
894
+ return sources.get(windowId);
895
+ },
896
+ /**
897
+ * Get a snapshot of all audio sources.
898
+ *
899
+ * @returns A new Map of all active audio sources
900
+ */
901
+ getSources() {
902
+ return new Map(sources);
903
+ },
904
+ /** Current version counter (incremented on every change). */
905
+ get version() {
906
+ return version;
907
+ },
908
+ /** Number of active audio sources. */
909
+ get count() {
910
+ return sources.size;
911
+ },
912
+ /** Clear all audio sources and reset version counter. */
913
+ clear() {
914
+ sources.clear();
915
+ version = 0;
916
+ }
917
+ };
918
+
919
+ // src/shell-init.ts
920
+ var CANONICAL_NUB_DOMAINS = [
921
+ "identity",
922
+ "storage",
923
+ "ifc",
924
+ "theme",
925
+ "keys",
926
+ "media",
927
+ "notify"
928
+ ];
929
+ function buildShellCapabilities(hooks) {
930
+ const nubs = hooks.relayPool ? ["relay", ...CANONICAL_NUB_DOMAINS] : [...CANONICAL_NUB_DOMAINS];
931
+ return { nubs, sandbox: [] };
932
+ }
933
+
934
+ // src/keys-forwarder.ts
935
+ function createKeysForwarder(deps) {
936
+ const target = deps.target ?? (typeof window !== "undefined" ? window : new EventTarget());
937
+ const listener = (ev) => {
938
+ const ke = ev;
939
+ const entries = deps.sessionRegistry.getAllEntries();
940
+ for (const entry of entries) {
941
+ if (!deps.hasKeysForwardCap(entry.pubkey)) continue;
942
+ const iframe = deps.originRegistry.getIframeWindow(entry.windowId);
943
+ if (!iframe) continue;
944
+ const envelope = {
945
+ type: "keys.forward",
946
+ key: ke.key ?? "",
947
+ code: ke.code ?? "",
948
+ ctrl: ke.ctrlKey ?? false,
949
+ alt: ke.altKey ?? false,
950
+ shift: ke.shiftKey ?? false,
951
+ meta: ke.metaKey ?? false
952
+ };
953
+ iframe.postMessage(envelope, "*");
954
+ }
955
+ };
956
+ target.addEventListener("keydown", listener);
957
+ return {
958
+ destroy() {
959
+ target.removeEventListener("keydown", listener);
960
+ }
961
+ };
962
+ }
963
+
964
+ // src/shell-bridge.ts
965
+ function createShellBridge(hooks) {
966
+ const runtimeHooks = adaptHooks(hooks, {
967
+ originRegistry,
968
+ manifestCache,
969
+ aclStore,
970
+ audioManager,
971
+ nappKeyRegistry
972
+ });
973
+ const runtime = createRuntime(runtimeHooks);
974
+ let keysForwarder = null;
975
+ if (typeof window !== "undefined") {
976
+ try {
977
+ keysForwarder = createKeysForwarder({
978
+ originRegistry,
979
+ sessionRegistry,
980
+ hasKeysForwardCap: (pubkey) => {
981
+ const entry = sessionRegistry.getEntry(pubkey);
982
+ if (!entry) return false;
983
+ const acl = aclStore.getEntry(entry.pubkey, entry.dTag, entry.aggregateHash);
984
+ return acl?.capabilities.includes("keys:forward") ?? false;
985
+ }
986
+ });
987
+ } catch {
988
+ keysForwarder = null;
989
+ }
990
+ }
991
+ return {
992
+ handleMessage(event) {
993
+ const sourceWindow = event.source;
994
+ if (!sourceWindow) return;
995
+ const windowId = originRegistry.getWindowId(sourceWindow);
996
+ if (!windowId) return;
997
+ const msg = event.data;
998
+ if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
999
+ if (msg.type === "shell.ready") {
1000
+ const capabilities = buildShellCapabilities(hooks);
1001
+ const initMsg = {
1002
+ type: "shell.init",
1003
+ capabilities,
1004
+ services: Object.keys(hooks.services ?? {})
1005
+ };
1006
+ const win = originRegistry.getIframeWindow(windowId);
1007
+ if (win) win.postMessage(initMsg, "*");
1008
+ return;
1009
+ }
1010
+ runtime.handleMessage(windowId, msg);
1011
+ },
1012
+ injectEvent(topic, payload) {
1013
+ runtime.injectEvent(topic, payload);
1014
+ },
1015
+ destroy() {
1016
+ keysForwarder?.destroy();
1017
+ runtime.destroy();
1018
+ },
1019
+ registerConsentHandler(handler) {
1020
+ runtime.registerConsentHandler(handler);
1021
+ },
1022
+ publishTheme(theme) {
1023
+ const envelope = { type: "theme.changed", theme };
1024
+ const windowIds = originRegistry.getAllWindowIds();
1025
+ for (const windowId of windowIds) {
1026
+ const win = originRegistry.getIframeWindow(windowId);
1027
+ if (!win) continue;
1028
+ win.postMessage(envelope, "*");
1029
+ }
1030
+ },
1031
+ get runtime() {
1032
+ return runtime;
1033
+ }
1034
+ };
1035
+ }
1036
+
1037
+ // src/index.ts
1038
+ import { ALL_CAPABILITIES as ALL_CAPABILITIES2 } from "@kehto/runtime";
1039
+ import { createEnforceGate, createNubEnforceGate, formatDenialReason } from "@kehto/runtime";
1040
+
1041
+ // src/topics.ts
1042
+ import { TOPICS } from "@napplet/core";
1043
+
1044
+ // src/identity-proxy.ts
1045
+ function createIdentityProxy(deps) {
1046
+ return {
1047
+ dispatch(windowId, envelope) {
1048
+ deps.runtime.handleMessage(windowId, envelope);
1049
+ },
1050
+ emit(windowId, envelope) {
1051
+ const win = deps.originRegistry.getIframeWindow(windowId);
1052
+ if (win) win.postMessage(envelope, "*");
1053
+ }
1054
+ };
1055
+ }
1056
+
1057
+ // src/theme-proxy.ts
1058
+ function createThemeProxy(deps) {
1059
+ return {
1060
+ dispatch(windowId, envelope) {
1061
+ deps.runtime.handleMessage(windowId, envelope);
1062
+ },
1063
+ emit(windowId, envelope) {
1064
+ const win = deps.originRegistry.getIframeWindow(windowId);
1065
+ if (win) win.postMessage(envelope, "*");
1066
+ }
1067
+ };
1068
+ }
1069
+
1070
+ // src/keys-proxy.ts
1071
+ function createKeysProxy(deps) {
1072
+ return {
1073
+ dispatch(windowId, envelope) {
1074
+ deps.runtime.handleMessage(windowId, envelope);
1075
+ },
1076
+ emit(windowId, envelope) {
1077
+ const win = deps.originRegistry.getIframeWindow(windowId);
1078
+ if (win) win.postMessage(envelope, "*");
1079
+ }
1080
+ };
1081
+ }
1082
+
1083
+ // src/media-proxy.ts
1084
+ function createMediaProxy(deps) {
1085
+ return {
1086
+ dispatch(windowId, envelope) {
1087
+ deps.runtime.handleMessage(windowId, envelope);
1088
+ },
1089
+ emit(windowId, envelope) {
1090
+ const win = deps.originRegistry.getIframeWindow(windowId);
1091
+ if (win) win.postMessage(envelope, "*");
1092
+ }
1093
+ };
1094
+ }
1095
+
1096
+ // src/notify-proxy.ts
1097
+ function createNotifyProxy(deps) {
1098
+ return {
1099
+ dispatch(windowId, envelope) {
1100
+ deps.runtime.handleMessage(windowId, envelope);
1101
+ },
1102
+ emit(windowId, envelope) {
1103
+ const win = deps.originRegistry.getIframeWindow(windowId);
1104
+ if (win) win.postMessage(envelope, "*");
1105
+ }
1106
+ };
1107
+ }
1108
+ export {
1109
+ ALL_CAPABILITIES2 as ALL_CAPABILITIES,
1110
+ TOPICS,
1111
+ adaptHooks,
1112
+ audioManager,
1113
+ buildShellCapabilities,
1114
+ createEnforceGate,
1115
+ createIdentityProxy,
1116
+ createKeysForwarder,
1117
+ createKeysProxy,
1118
+ createMediaProxy,
1119
+ createNotifyProxy,
1120
+ createNubEnforceGate,
1121
+ createShellBridge,
1122
+ createThemeProxy,
1123
+ formatDenialReason,
1124
+ manifestCache,
1125
+ nappKeyRegistry,
1126
+ originRegistry,
1127
+ sessionRegistry
1128
+ };
1129
+ //# sourceMappingURL=index.js.map