@kehto/runtime 0.1.0 → 0.5.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 +25 -20
- package/dist/index.d.ts +49 -58
- package/dist/index.js +796 -879
- package/dist/index.js.map +1 -1
- package/package.json +17 -31
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// src/enforce.ts
|
|
2
|
+
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
2
3
|
import { resolveCapabilitiesNub } from "@kehto/acl";
|
|
4
|
+
var CLASS_CAPABILITY_ALLOWLIST = Object.freeze({
|
|
5
|
+
"class-1": new Set(ALL_CAPABILITIES),
|
|
6
|
+
"class-2": new Set(ALL_CAPABILITIES.filter(
|
|
7
|
+
(c) => c !== "relay:write" && c !== "identity:decrypt"
|
|
8
|
+
))
|
|
9
|
+
});
|
|
3
10
|
function createEnforceGate(config) {
|
|
4
11
|
const { checkAcl, resolveIdentity, onAclCheck } = config;
|
|
5
12
|
return function enforce(pubkey, capability, message) {
|
|
@@ -9,10 +16,11 @@ function createEnforceGate(config) {
|
|
|
9
16
|
const allowed = checkAcl(pubkey, dTag, aggregateHash, capability);
|
|
10
17
|
const identity = { pubkey, dTag, hash: aggregateHash };
|
|
11
18
|
const decision = allowed ? "allow" : "deny";
|
|
19
|
+
const reason = allowed ? "allowed" : "capability-missing";
|
|
12
20
|
if (onAclCheck) {
|
|
13
|
-
onAclCheck({ identity, capability, decision, message });
|
|
21
|
+
onAclCheck({ identity, capability, decision, message, reason });
|
|
14
22
|
}
|
|
15
|
-
return { allowed, capability };
|
|
23
|
+
return { allowed, capability, reason };
|
|
16
24
|
};
|
|
17
25
|
}
|
|
18
26
|
function createNubEnforceGate(config) {
|
|
@@ -21,13 +29,31 @@ function createNubEnforceGate(config) {
|
|
|
21
29
|
const entry = resolveIdentityByWindowId(windowId);
|
|
22
30
|
const dTag = entry?.dTag ?? "";
|
|
23
31
|
const aggregateHash = entry?.aggregateHash ?? "";
|
|
32
|
+
const nappletClass = entry?.class ?? null;
|
|
33
|
+
if (nappletClass !== null) {
|
|
34
|
+
const allowlist = CLASS_CAPABILITY_ALLOWLIST[nappletClass];
|
|
35
|
+
if (!allowlist || !allowlist.has(capability)) {
|
|
36
|
+
const identity2 = { pubkey: "", dTag, hash: aggregateHash };
|
|
37
|
+
if (onAclCheck) {
|
|
38
|
+
onAclCheck({
|
|
39
|
+
identity: identity2,
|
|
40
|
+
capability,
|
|
41
|
+
decision: "deny",
|
|
42
|
+
message,
|
|
43
|
+
reason: "class-forbidden"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { allowed: false, capability, reason: "class-forbidden" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
24
49
|
const allowed = checkAcl("", dTag, aggregateHash, capability);
|
|
25
50
|
const identity = { pubkey: "", dTag, hash: aggregateHash };
|
|
26
51
|
const decision = allowed ? "allow" : "deny";
|
|
52
|
+
const reason = allowed ? "allowed" : "capability-missing";
|
|
27
53
|
if (onAclCheck) {
|
|
28
|
-
onAclCheck({ identity, capability, decision, message });
|
|
54
|
+
onAclCheck({ identity, capability, decision, message, reason });
|
|
29
55
|
}
|
|
30
|
-
return { allowed, capability };
|
|
56
|
+
return { allowed, capability, reason };
|
|
31
57
|
};
|
|
32
58
|
}
|
|
33
59
|
function formatDenialReason(capability) {
|
|
@@ -117,7 +143,7 @@ import {
|
|
|
117
143
|
CAP_HOTKEY_FORWARD,
|
|
118
144
|
CAP_STATE_READ,
|
|
119
145
|
CAP_STATE_WRITE,
|
|
120
|
-
|
|
146
|
+
toKey
|
|
121
147
|
} from "@kehto/acl";
|
|
122
148
|
var CAP_IDENTITY_READ = 1 << 5;
|
|
123
149
|
var CAP_KEYS_BIND = 1 << 6;
|
|
@@ -126,6 +152,10 @@ var CAP_MEDIA_CONTROL = 1 << 10;
|
|
|
126
152
|
var CAP_NOTIFY_SEND = 1 << 11;
|
|
127
153
|
var CAP_NOTIFY_CHANNEL = 1 << 12;
|
|
128
154
|
var CAP_THEME_READ = 1 << 13;
|
|
155
|
+
var CAP_CONFIG_READ = 1 << 14;
|
|
156
|
+
var CAP_RESOURCE_FETCH = 1 << 15;
|
|
157
|
+
var CAP_IDENTITY_DECRYPT = 1 << 16;
|
|
158
|
+
var CAP_CVM_CALL = 1 << 17;
|
|
129
159
|
var CAP_MAP = {
|
|
130
160
|
"relay:read": CAP_RELAY_READ,
|
|
131
161
|
"relay:write": CAP_RELAY_WRITE,
|
|
@@ -140,8 +170,13 @@ var CAP_MAP = {
|
|
|
140
170
|
"media:control": CAP_MEDIA_CONTROL,
|
|
141
171
|
"notify:send": CAP_NOTIFY_SEND,
|
|
142
172
|
"notify:channel": CAP_NOTIFY_CHANNEL,
|
|
143
|
-
"theme:read": CAP_THEME_READ
|
|
173
|
+
"theme:read": CAP_THEME_READ,
|
|
174
|
+
"config:read": CAP_CONFIG_READ,
|
|
175
|
+
"resource:fetch": CAP_RESOURCE_FETCH,
|
|
176
|
+
"identity:decrypt": CAP_IDENTITY_DECRYPT,
|
|
177
|
+
"cvm:call": CAP_CVM_CALL
|
|
144
178
|
};
|
|
179
|
+
var RUNTIME_CAP_ALL = Object.values(CAP_MAP).reduce((bits, bit) => bits | bit, 0);
|
|
145
180
|
function capToBit(cap) {
|
|
146
181
|
return CAP_MAP[cap] ?? 0;
|
|
147
182
|
}
|
|
@@ -157,6 +192,11 @@ function toIdentity(pubkey, dTag, hash) {
|
|
|
157
192
|
}
|
|
158
193
|
function createAclState(persistence, defaultPolicy = "permissive") {
|
|
159
194
|
let state = createState(defaultPolicy);
|
|
195
|
+
function ensureRuntimeDefaultEntry(id) {
|
|
196
|
+
if (state.defaultPolicy !== "permissive") return;
|
|
197
|
+
if (state.entries[toKey(id)]) return;
|
|
198
|
+
state = grant(state, id, RUNTIME_CAP_ALL);
|
|
199
|
+
}
|
|
160
200
|
return {
|
|
161
201
|
check(pubkey, dTag, aggregateHash, capability) {
|
|
162
202
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
@@ -164,23 +204,27 @@ function createAclState(persistence, defaultPolicy = "permissive") {
|
|
|
164
204
|
},
|
|
165
205
|
grant(pubkey, dTag, aggregateHash, capability) {
|
|
166
206
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
207
|
+
ensureRuntimeDefaultEntry(id);
|
|
167
208
|
state = grant(state, id, capToBit(capability));
|
|
168
209
|
},
|
|
169
210
|
revoke(pubkey, dTag, aggregateHash, capability) {
|
|
170
211
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
212
|
+
ensureRuntimeDefaultEntry(id);
|
|
171
213
|
state = revoke(state, id, capToBit(capability));
|
|
172
214
|
},
|
|
173
215
|
block(pubkey, dTag, aggregateHash) {
|
|
174
216
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
217
|
+
ensureRuntimeDefaultEntry(id);
|
|
175
218
|
state = block(state, id);
|
|
176
219
|
},
|
|
177
220
|
unblock(pubkey, dTag, aggregateHash) {
|
|
178
221
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
222
|
+
ensureRuntimeDefaultEntry(id);
|
|
179
223
|
state = unblock(state, id);
|
|
180
224
|
},
|
|
181
225
|
isBlocked(pubkey, dTag, aggregateHash) {
|
|
182
226
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
183
|
-
return !check(state, id,
|
|
227
|
+
return !check(state, id, RUNTIME_CAP_ALL) && this.getEntry(pubkey, dTag, aggregateHash)?.blocked === true;
|
|
184
228
|
},
|
|
185
229
|
getEntry(pubkey, dTag, aggregateHash) {
|
|
186
230
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
@@ -195,8 +239,7 @@ function createAclState(persistence, defaultPolicy = "permissive") {
|
|
|
195
239
|
};
|
|
196
240
|
},
|
|
197
241
|
getAllEntries() {
|
|
198
|
-
return Object.entries(state.entries).map(([
|
|
199
|
-
const parts = key.split(":");
|
|
242
|
+
return Object.entries(state.entries).map(([, entry]) => {
|
|
200
243
|
return {
|
|
201
244
|
pubkey: "",
|
|
202
245
|
capabilities: bitsToCapabilities(entry.caps),
|
|
@@ -391,22 +434,21 @@ function createEventBuffer(sendToNapplet, sessionRegistry, enforce, subscription
|
|
|
391
434
|
|
|
392
435
|
// src/runtime.ts
|
|
393
436
|
import { createDispatch } from "@napplet/core";
|
|
394
|
-
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
395
437
|
|
|
396
438
|
// src/service-dispatch.ts
|
|
397
439
|
function routeServiceMessage(windowId, message, services, sendToNapplet) {
|
|
398
|
-
const send = (msg) => sendToNapplet(windowId, msg);
|
|
399
440
|
const domain = message.type.split(".")[0];
|
|
400
441
|
const handler = services[domain];
|
|
401
442
|
if (handler) {
|
|
402
|
-
handler.handleMessage(windowId, message,
|
|
443
|
+
handler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
403
444
|
return true;
|
|
404
445
|
}
|
|
405
|
-
|
|
406
|
-
|
|
446
|
+
const ifcMessage = message;
|
|
447
|
+
if (message.type === "ifc.emit" && typeof ifcMessage.topic === "string") {
|
|
448
|
+
const prefix = ifcMessage.topic.split(":")[0];
|
|
407
449
|
const ifcHandler = services[prefix];
|
|
408
450
|
if (ifcHandler) {
|
|
409
|
-
ifcHandler.handleMessage(windowId, message,
|
|
451
|
+
ifcHandler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
410
452
|
return true;
|
|
411
453
|
}
|
|
412
454
|
}
|
|
@@ -421,6 +463,492 @@ function notifyServiceWindowDestroyed(windowId, services) {
|
|
|
421
463
|
}
|
|
422
464
|
}
|
|
423
465
|
|
|
466
|
+
// src/relay-handler.ts
|
|
467
|
+
function createRelayHandler(context) {
|
|
468
|
+
return function handleRelayMessage(windowId, msg) {
|
|
469
|
+
const m = msg;
|
|
470
|
+
const dotIdx = msg.type.indexOf(".");
|
|
471
|
+
const action = msg.type.slice(dotIdx + 1);
|
|
472
|
+
switch (action) {
|
|
473
|
+
case "subscribe":
|
|
474
|
+
handleRelaySubscribe(context, windowId, msg, m);
|
|
475
|
+
return;
|
|
476
|
+
case "close":
|
|
477
|
+
handleRelayClose(context, windowId, msg, m);
|
|
478
|
+
return;
|
|
479
|
+
case "publish":
|
|
480
|
+
handleRelayPublish(context, windowId, msg, m);
|
|
481
|
+
return;
|
|
482
|
+
case "publishEncrypted":
|
|
483
|
+
handleRelayPublishEncrypted(context, windowId, msg);
|
|
484
|
+
return;
|
|
485
|
+
case "query":
|
|
486
|
+
handleRelayQuery(context, windowId, m);
|
|
487
|
+
return;
|
|
488
|
+
default:
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function relayServiceFrom(context) {
|
|
494
|
+
return context.serviceRegistry["relay"] ?? context.serviceRegistry["relay-pool"];
|
|
495
|
+
}
|
|
496
|
+
function isShellKindQuery(filters) {
|
|
497
|
+
return filters.length > 0 && filters.every((filter) => filter.kinds?.every((kind) => kind >= 29e3 && kind < 3e4));
|
|
498
|
+
}
|
|
499
|
+
function handleRelaySubscribe(context, windowId, msg, m) {
|
|
500
|
+
const { eventBuffer, hooks, serviceRegistry, subscriptions } = context;
|
|
501
|
+
const subId = m.subId ?? "";
|
|
502
|
+
const filters = m.filters ?? [];
|
|
503
|
+
if (!subId) return;
|
|
504
|
+
const subKey = `${windowId}:${subId}`;
|
|
505
|
+
subscriptions.set(subKey, { windowId, filters });
|
|
506
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
507
|
+
function deliver(event) {
|
|
508
|
+
if (seenIds.has(event.id)) return;
|
|
509
|
+
seenIds.add(event.id);
|
|
510
|
+
if (subscriptions.has(subKey)) {
|
|
511
|
+
hooks.sendToNapplet(windowId, { type: "relay.event", subId, event });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const bufferedEvent of eventBuffer.getBufferedEvents()) {
|
|
515
|
+
if (matchesAnyFilter(bufferedEvent, filters)) deliver(bufferedEvent);
|
|
516
|
+
}
|
|
517
|
+
const isShellKind = isShellKindQuery(filters);
|
|
518
|
+
const relayService = relayServiceFrom(context);
|
|
519
|
+
const cacheService = !serviceRegistry["relay"] ? serviceRegistry["cache"] : void 0;
|
|
520
|
+
if (!isShellKind && relayService) {
|
|
521
|
+
relayService.handleMessage(windowId, msg, (resp) => {
|
|
522
|
+
if (!subscriptions.has(subKey)) return;
|
|
523
|
+
hooks.sendToNapplet(windowId, resp);
|
|
524
|
+
});
|
|
525
|
+
if (cacheService) {
|
|
526
|
+
cacheService.handleMessage(windowId, msg, (resp) => {
|
|
527
|
+
if (!subscriptions.has(subKey)) return;
|
|
528
|
+
hooks.sendToNapplet(windowId, resp);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, isShellKind, deliver);
|
|
534
|
+
}
|
|
535
|
+
function deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, isShellKind, deliver) {
|
|
536
|
+
const { hooks } = context;
|
|
537
|
+
const cache = hooks.cache;
|
|
538
|
+
if (cache?.isAvailable() && !isShellKind) {
|
|
539
|
+
cache.query(filters).then((cachedEvents) => {
|
|
540
|
+
for (const event of cachedEvents) deliver(event);
|
|
541
|
+
}).catch(() => {
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const pool = hooks.relayPool;
|
|
545
|
+
if (!pool?.isAvailable() && !isShellKind) {
|
|
546
|
+
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (!pool?.isAvailable() || isShellKind) return;
|
|
550
|
+
const relayUrls = pool.selectRelayTier(filters);
|
|
551
|
+
let eoseSent = false;
|
|
552
|
+
const eoseFallbackTimer = setTimeout(() => {
|
|
553
|
+
if (!eoseSent) {
|
|
554
|
+
eoseSent = true;
|
|
555
|
+
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
556
|
+
}
|
|
557
|
+
}, 15e3);
|
|
558
|
+
const subscription = pool.subscribe(filters, (item) => {
|
|
559
|
+
if (item === "EOSE") {
|
|
560
|
+
clearTimeout(eoseFallbackTimer);
|
|
561
|
+
if (!eoseSent) {
|
|
562
|
+
eoseSent = true;
|
|
563
|
+
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
deliver(item);
|
|
568
|
+
if (cache?.isAvailable() && !isShellKind) {
|
|
569
|
+
try {
|
|
570
|
+
cache.store(item);
|
|
571
|
+
} catch {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}, relayUrls);
|
|
576
|
+
pool.trackSubscription(subKey, () => {
|
|
577
|
+
clearTimeout(eoseFallbackTimer);
|
|
578
|
+
subscription.unsubscribe();
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function handleRelayClose(context, windowId, msg, m) {
|
|
582
|
+
const { hooks, subscriptions } = context;
|
|
583
|
+
const subId = m.subId ?? "";
|
|
584
|
+
if (!subId) return;
|
|
585
|
+
const subKey = `${windowId}:${subId}`;
|
|
586
|
+
subscriptions.delete(subKey);
|
|
587
|
+
const relayService = relayServiceFrom(context);
|
|
588
|
+
if (relayService) relayService.handleMessage(windowId, msg, () => {
|
|
589
|
+
});
|
|
590
|
+
hooks.relayPool?.untrackSubscription(subKey);
|
|
591
|
+
hooks.sendToNapplet(windowId, { type: "relay.closed", subId, message: "" });
|
|
592
|
+
}
|
|
593
|
+
function handleRelayPublish(context, windowId, msg, m) {
|
|
594
|
+
const { eventBuffer, hooks, replayDetector } = context;
|
|
595
|
+
const event = m.event;
|
|
596
|
+
const id = m.id ?? "";
|
|
597
|
+
if (!event || typeof event !== "object") {
|
|
598
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.error", id, error: "invalid event" });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const replayResult = replayDetector.check(event);
|
|
602
|
+
if (replayResult !== null) {
|
|
603
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: replayResult });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const relayService = relayServiceFrom(context);
|
|
607
|
+
if (relayService) {
|
|
608
|
+
relayService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
609
|
+
} else if (hooks.relayPool?.isAvailable()) {
|
|
610
|
+
hooks.relayPool.publish(event);
|
|
611
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: true });
|
|
612
|
+
} else {
|
|
613
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: "no relay pool available" });
|
|
614
|
+
}
|
|
615
|
+
eventBuffer.bufferAndDeliver(event, windowId);
|
|
616
|
+
}
|
|
617
|
+
function handleRelayPublishEncrypted(context, windowId, msg) {
|
|
618
|
+
const { hooks } = context;
|
|
619
|
+
const id = msg.id ?? "";
|
|
620
|
+
const eventTemplate = msg.event;
|
|
621
|
+
const peMsg = msg;
|
|
622
|
+
const recipient = peMsg.recipient ?? "";
|
|
623
|
+
const encryption = peMsg.encryption ?? "nip44";
|
|
624
|
+
const replyPe = (ok, extra = {}) => {
|
|
625
|
+
hooks.sendToNapplet(windowId, { type: "relay.publishEncrypted.result", id, ok, ...extra });
|
|
626
|
+
};
|
|
627
|
+
if (!recipient) {
|
|
628
|
+
replyPe(false, { error: "missing recipient" });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (encryption !== "nip44" && encryption !== "nip04") {
|
|
632
|
+
replyPe(false, { error: `unsupported encryption scheme: ${encryption}` });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const peSigner = hooks.auth.getSigner();
|
|
636
|
+
if (!peSigner) {
|
|
637
|
+
replyPe(false, { error: "no signer configured" });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (!eventTemplate || typeof eventTemplate !== "object") {
|
|
641
|
+
replyPe(false, { error: "invalid event template" });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe);
|
|
645
|
+
}
|
|
646
|
+
function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe) {
|
|
647
|
+
const { eventBuffer, hooks } = context;
|
|
648
|
+
const peSigner = hooks.auth.getSigner();
|
|
649
|
+
if (!peSigner) return;
|
|
650
|
+
(async () => {
|
|
651
|
+
try {
|
|
652
|
+
const plaintext = String(eventTemplate.content ?? "");
|
|
653
|
+
const ciphertext = encryption === "nip44" ? await peSigner.nip44?.encrypt(recipient, plaintext) ?? "" : await peSigner.nip04?.encrypt(recipient, plaintext) ?? "";
|
|
654
|
+
const eventWithCiphertext = { ...eventTemplate, content: ciphertext };
|
|
655
|
+
const signed = await peSigner.signEvent?.(eventWithCiphertext);
|
|
656
|
+
if (!signed) {
|
|
657
|
+
replyPe(false, { error: "signEvent returned null" });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
publishSignedEncrypted(context, windowId, id, signed, replyPe);
|
|
661
|
+
try {
|
|
662
|
+
eventBuffer.bufferAndDeliver(signed, windowId);
|
|
663
|
+
} catch {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
} catch (err) {
|
|
667
|
+
replyPe(false, { error: err?.message ?? "encryption failed" });
|
|
668
|
+
}
|
|
669
|
+
})();
|
|
670
|
+
}
|
|
671
|
+
function publishSignedEncrypted(context, windowId, id, signed, replyPe) {
|
|
672
|
+
const { hooks } = context;
|
|
673
|
+
const relayService = relayServiceFrom(context);
|
|
674
|
+
if (!relayService) {
|
|
675
|
+
if (hooks.relayPool?.isAvailable()) {
|
|
676
|
+
hooks.relayPool.publish(signed);
|
|
677
|
+
replyPe(true, { event: signed, eventId: signed.id });
|
|
678
|
+
} else {
|
|
679
|
+
replyPe(false, { error: "no relay pool available" });
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const publishMsg = { type: "relay.publish", id, event: signed };
|
|
684
|
+
let replied = false;
|
|
685
|
+
relayService.handleMessage(windowId, publishMsg, (resp) => {
|
|
686
|
+
if (replied) return;
|
|
687
|
+
const r = resp;
|
|
688
|
+
if (typeof r.type !== "string" || !r.type.startsWith("relay.publish")) return;
|
|
689
|
+
const okVal = r.ok ?? r.accepted ?? false;
|
|
690
|
+
replied = true;
|
|
691
|
+
const publishResult = { event: signed, eventId: signed.id };
|
|
692
|
+
if (!okVal) publishResult.error = r.error ?? r.message ?? "publish failed";
|
|
693
|
+
replyPe(okVal, publishResult);
|
|
694
|
+
});
|
|
695
|
+
if (!replied) {
|
|
696
|
+
replied = true;
|
|
697
|
+
replyPe(true, { event: signed, eventId: signed.id });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function handleRelayQuery(context, windowId, m) {
|
|
701
|
+
const id = m.id ?? "";
|
|
702
|
+
const filters = m.filters ?? [];
|
|
703
|
+
let count = 0;
|
|
704
|
+
for (const event of context.eventBuffer.getBufferedEvents()) {
|
|
705
|
+
if (matchesAnyFilter(event, filters)) count++;
|
|
706
|
+
}
|
|
707
|
+
context.hooks.sendToNapplet(windowId, { type: "relay.query.result", id, count });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/identity-handler.ts
|
|
711
|
+
function createIdentityHandler(context) {
|
|
712
|
+
return function handleIdentityMessage(windowId, msg) {
|
|
713
|
+
const { hooks, serviceRegistry } = context;
|
|
714
|
+
const identityService = serviceRegistry["identity"];
|
|
715
|
+
if (identityService) {
|
|
716
|
+
identityService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const id = msg.id ?? "";
|
|
720
|
+
const action = msg.type.slice("identity.".length);
|
|
721
|
+
const signer = hooks.auth.getSigner();
|
|
722
|
+
const sendError = (error) => {
|
|
723
|
+
hooks.sendToNapplet(windowId, { type: `${msg.type}.error`, id, error });
|
|
724
|
+
};
|
|
725
|
+
const sendResult = (payload) => {
|
|
726
|
+
hooks.sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
727
|
+
};
|
|
728
|
+
switch (action) {
|
|
729
|
+
case "getPublicKey":
|
|
730
|
+
if (!signer) {
|
|
731
|
+
sendResult({ pubkey: "" });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey: pubkey ?? "" })).catch((err) => sendError(err?.message ?? "getPublicKey failed"));
|
|
735
|
+
return;
|
|
736
|
+
case "getRelays":
|
|
737
|
+
if (!signer) {
|
|
738
|
+
sendError("no signer configured");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch((err) => sendError(err?.message ?? "getRelays failed"));
|
|
742
|
+
return;
|
|
743
|
+
case "getProfile":
|
|
744
|
+
sendResult({ profile: null });
|
|
745
|
+
return;
|
|
746
|
+
case "getFollows":
|
|
747
|
+
sendResult({ pubkeys: [] });
|
|
748
|
+
return;
|
|
749
|
+
case "getList":
|
|
750
|
+
sendResult({ entries: [] });
|
|
751
|
+
return;
|
|
752
|
+
case "getZaps":
|
|
753
|
+
sendResult({ zaps: [] });
|
|
754
|
+
return;
|
|
755
|
+
case "getMutes":
|
|
756
|
+
sendResult({ pubkeys: [] });
|
|
757
|
+
return;
|
|
758
|
+
case "getBlocked":
|
|
759
|
+
sendResult({ pubkeys: [] });
|
|
760
|
+
return;
|
|
761
|
+
case "getBadges":
|
|
762
|
+
sendResult({ badges: [] });
|
|
763
|
+
return;
|
|
764
|
+
default:
|
|
765
|
+
sendError(`Unknown identity action: ${action}`);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/ifc-handler.ts
|
|
771
|
+
function createIfcRuntime(hooks, sessionRegistry) {
|
|
772
|
+
const state = {
|
|
773
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
774
|
+
channels: /* @__PURE__ */ new Map(),
|
|
775
|
+
channelsByWindow: /* @__PURE__ */ new Map()
|
|
776
|
+
};
|
|
777
|
+
return {
|
|
778
|
+
handleMessage(windowId, msg) {
|
|
779
|
+
handleIfcMessage(state, hooks, sessionRegistry, windowId, msg);
|
|
780
|
+
},
|
|
781
|
+
destroyWindow(windowId) {
|
|
782
|
+
removeWindowChannels(state, hooks, windowId);
|
|
783
|
+
removeWindowSubscriptions(state, windowId);
|
|
784
|
+
},
|
|
785
|
+
clear() {
|
|
786
|
+
state.subscriptions.clear();
|
|
787
|
+
state.channels.clear();
|
|
788
|
+
state.channelsByWindow.clear();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function addChannel(state, channelId, peerA, peerB) {
|
|
793
|
+
state.channels.set(channelId, { channelId, peerA, peerB });
|
|
794
|
+
for (const windowId of [peerA, peerB]) {
|
|
795
|
+
let set = state.channelsByWindow.get(windowId);
|
|
796
|
+
if (!set) {
|
|
797
|
+
set = /* @__PURE__ */ new Set();
|
|
798
|
+
state.channelsByWindow.set(windowId, set);
|
|
799
|
+
}
|
|
800
|
+
set.add(channelId);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function removeChannel(state, channelId) {
|
|
804
|
+
const channel = state.channels.get(channelId);
|
|
805
|
+
if (!channel) return;
|
|
806
|
+
state.channels.delete(channelId);
|
|
807
|
+
for (const windowId of [channel.peerA, channel.peerB]) {
|
|
808
|
+
const set = state.channelsByWindow.get(windowId);
|
|
809
|
+
if (!set) continue;
|
|
810
|
+
set.delete(channelId);
|
|
811
|
+
if (set.size === 0) state.channelsByWindow.delete(windowId);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function peerOf(state, channelId, self) {
|
|
815
|
+
const channel = state.channels.get(channelId);
|
|
816
|
+
if (!channel) return null;
|
|
817
|
+
if (channel.peerA === self) return channel.peerB;
|
|
818
|
+
if (channel.peerB === self) return channel.peerA;
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function resolveTarget(sessionRegistry, target) {
|
|
822
|
+
if (sessionRegistry.getEntryByWindowId(target)) return target;
|
|
823
|
+
const entries = sessionRegistry.getAllEntries();
|
|
824
|
+
const byPubkey = entries.find((entry) => entry.pubkey === target);
|
|
825
|
+
return byPubkey?.windowId ?? null;
|
|
826
|
+
}
|
|
827
|
+
function handleIfcMessage(state, hooks, sessionRegistry, windowId, msg) {
|
|
828
|
+
const m = msg;
|
|
829
|
+
const dotIdx = msg.type.indexOf(".");
|
|
830
|
+
const action = msg.type.slice(dotIdx + 1);
|
|
831
|
+
switch (action) {
|
|
832
|
+
case "emit":
|
|
833
|
+
handleEmit(state, hooks, windowId, m);
|
|
834
|
+
return;
|
|
835
|
+
case "subscribe":
|
|
836
|
+
handleSubscribe(state, hooks, windowId, m);
|
|
837
|
+
return;
|
|
838
|
+
case "unsubscribe":
|
|
839
|
+
handleUnsubscribe(state, windowId, m);
|
|
840
|
+
return;
|
|
841
|
+
case "channel.open":
|
|
842
|
+
handleChannelOpen(state, hooks, sessionRegistry, windowId, m);
|
|
843
|
+
return;
|
|
844
|
+
case "channel.emit":
|
|
845
|
+
handleChannelEmit(state, hooks, windowId, m);
|
|
846
|
+
return;
|
|
847
|
+
case "channel.broadcast":
|
|
848
|
+
handleChannelBroadcast(state, hooks, windowId, m);
|
|
849
|
+
return;
|
|
850
|
+
case "channel.list":
|
|
851
|
+
handleChannelList(state, hooks, windowId, m);
|
|
852
|
+
return;
|
|
853
|
+
case "channel.close":
|
|
854
|
+
handleChannelClose(state, hooks, windowId, m);
|
|
855
|
+
return;
|
|
856
|
+
default:
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function handleEmit(state, hooks, windowId, m) {
|
|
861
|
+
const topic = m.topic ?? "";
|
|
862
|
+
if (!topic) return;
|
|
863
|
+
const subscribers = state.subscriptions.get(topic);
|
|
864
|
+
if (!subscribers) return;
|
|
865
|
+
for (const subscriberWindowId of subscribers) {
|
|
866
|
+
if (subscriberWindowId !== windowId) {
|
|
867
|
+
hooks.sendToNapplet(subscriberWindowId, { type: "ifc.event", topic, payload: m.payload, sender: windowId });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function handleSubscribe(state, hooks, windowId, m) {
|
|
872
|
+
const id = m.id ?? "";
|
|
873
|
+
const topic = m.topic ?? "";
|
|
874
|
+
if (!topic) {
|
|
875
|
+
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id, error: "missing topic" });
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
let subscriptions = state.subscriptions.get(topic);
|
|
879
|
+
if (!subscriptions) {
|
|
880
|
+
subscriptions = /* @__PURE__ */ new Set();
|
|
881
|
+
state.subscriptions.set(topic, subscriptions);
|
|
882
|
+
}
|
|
883
|
+
subscriptions.add(windowId);
|
|
884
|
+
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id });
|
|
885
|
+
}
|
|
886
|
+
function handleUnsubscribe(state, windowId, m) {
|
|
887
|
+
const topic = m.topic ?? "";
|
|
888
|
+
if (!topic) return;
|
|
889
|
+
const subscriptions = state.subscriptions.get(topic);
|
|
890
|
+
if (!subscriptions) return;
|
|
891
|
+
subscriptions.delete(windowId);
|
|
892
|
+
if (subscriptions.size === 0) state.subscriptions.delete(topic);
|
|
893
|
+
}
|
|
894
|
+
function handleChannelOpen(state, hooks, sessionRegistry, windowId, m) {
|
|
895
|
+
const id = m.id ?? "";
|
|
896
|
+
const peerWindow = resolveTarget(sessionRegistry, m.target ?? "");
|
|
897
|
+
if (!peerWindow) {
|
|
898
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, error: "target not found" });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const channelId = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
|
|
902
|
+
addChannel(state, channelId, windowId, peerWindow);
|
|
903
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, channelId, peer: peerWindow });
|
|
904
|
+
}
|
|
905
|
+
function handleChannelEmit(state, hooks, windowId, m) {
|
|
906
|
+
const peer = peerOf(state, m.channelId ?? "", windowId);
|
|
907
|
+
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId: m.channelId ?? "", sender: windowId, payload: m.payload });
|
|
908
|
+
}
|
|
909
|
+
function handleChannelBroadcast(state, hooks, windowId, m) {
|
|
910
|
+
const channels = state.channelsByWindow.get(windowId);
|
|
911
|
+
if (!channels) return;
|
|
912
|
+
for (const channelId of channels) {
|
|
913
|
+
const peer = peerOf(state, channelId, windowId);
|
|
914
|
+
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function handleChannelList(state, hooks, windowId, m) {
|
|
918
|
+
const channels = [];
|
|
919
|
+
const set = state.channelsByWindow.get(windowId);
|
|
920
|
+
if (set) {
|
|
921
|
+
for (const channelId of set) {
|
|
922
|
+
const peer = peerOf(state, channelId, windowId);
|
|
923
|
+
if (peer) channels.push({ id: channelId, peer });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.list.result", id: m.id ?? "", channels });
|
|
927
|
+
}
|
|
928
|
+
function handleChannelClose(state, hooks, windowId, m) {
|
|
929
|
+
const channelId = m.channelId ?? "";
|
|
930
|
+
const peer = peerOf(state, channelId, windowId);
|
|
931
|
+
if (!peer) return;
|
|
932
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.closed", channelId });
|
|
933
|
+
hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
934
|
+
removeChannel(state, channelId);
|
|
935
|
+
}
|
|
936
|
+
function removeWindowSubscriptions(state, windowId) {
|
|
937
|
+
for (const [topic, subscriptions] of state.subscriptions) {
|
|
938
|
+
subscriptions.delete(windowId);
|
|
939
|
+
if (subscriptions.size === 0) state.subscriptions.delete(topic);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function removeWindowChannels(state, hooks, windowId) {
|
|
943
|
+
const channelIds = state.channelsByWindow.get(windowId);
|
|
944
|
+
if (!channelIds) return;
|
|
945
|
+
for (const channelId of Array.from(channelIds)) {
|
|
946
|
+
const peer = peerOf(state, channelId, windowId);
|
|
947
|
+
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
948
|
+
removeChannel(state, channelId);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
424
952
|
// src/state-handler.ts
|
|
425
953
|
function scopedKey(dTag, aggregateHash, userKey) {
|
|
426
954
|
return `napplet-state:${dTag}:${aggregateHash}:${userKey}`;
|
|
@@ -450,7 +978,7 @@ function handleStorageNub(windowId, msg, sendToNapplet, sessionRegistry, aclStat
|
|
|
450
978
|
sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
451
979
|
}
|
|
452
980
|
function sendErrorNub(error) {
|
|
453
|
-
sendToNapplet(windowId, { type: `${msg.type}.
|
|
981
|
+
sendToNapplet(windowId, { type: `${msg.type}.result`, id, error });
|
|
454
982
|
}
|
|
455
983
|
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
456
984
|
if (!entry) {
|
|
@@ -509,7 +1037,7 @@ function handleStorageNub(windowId, msg, sendToNapplet, sessionRegistry, aclStat
|
|
|
509
1037
|
break;
|
|
510
1038
|
}
|
|
511
1039
|
case "clear": {
|
|
512
|
-
sendErrorNub("storage.clear is not in @napplet/nub
|
|
1040
|
+
sendErrorNub("storage.clear is not in @napplet/nub/storage; action not supported");
|
|
513
1041
|
break;
|
|
514
1042
|
}
|
|
515
1043
|
case "keys": {
|
|
@@ -533,14 +1061,132 @@ function cleanupNappState(pubkey, dTag, aggregateHash, statePersistence) {
|
|
|
533
1061
|
statePersistence.clear(legacyPrefix);
|
|
534
1062
|
}
|
|
535
1063
|
|
|
1064
|
+
// src/domain-handlers.ts
|
|
1065
|
+
var THEME_FALLBACK_DEFAULT = {
|
|
1066
|
+
colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
|
|
1067
|
+
};
|
|
1068
|
+
function createRuntimeDomainHandlers(context) {
|
|
1069
|
+
return {
|
|
1070
|
+
storage: (windowId, msg) => handleStorageMessage(context, windowId, msg),
|
|
1071
|
+
media: (windowId, msg) => handleMediaMessage(context, windowId, msg),
|
|
1072
|
+
keys: (windowId, msg) => handleKeysMessage(context, windowId, msg),
|
|
1073
|
+
notify: (windowId, msg) => handleNotifyMessage(context, windowId, msg),
|
|
1074
|
+
theme: (windowId, msg) => handleThemeMessage(context, windowId, msg),
|
|
1075
|
+
config: (windowId, msg) => handleServiceOnlyMessage(context, "config", windowId, msg),
|
|
1076
|
+
resource: (windowId, msg) => handleServiceOnlyMessage(context, "resource", windowId, msg),
|
|
1077
|
+
cvm: (windowId, msg) => handleServiceOnlyMessage(context, "cvm", windowId, msg)
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function handleStorageMessage(context, windowId, msg) {
|
|
1081
|
+
const { aclState, hooks, sessionRegistry } = context;
|
|
1082
|
+
handleStorageNub(windowId, msg, hooks.sendToNapplet, sessionRegistry, aclState, hooks.statePersistence);
|
|
1083
|
+
}
|
|
1084
|
+
function handleMediaMessage(context, windowId, msg) {
|
|
1085
|
+
const { hooks, serviceRegistry } = context;
|
|
1086
|
+
const mediaService = serviceRegistry["media"];
|
|
1087
|
+
if (mediaService) {
|
|
1088
|
+
mediaService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (msg.type === "media.session.create") {
|
|
1092
|
+
const m = msg;
|
|
1093
|
+
if (m.owner !== "napplet" && m.owner !== "shell") {
|
|
1094
|
+
hooks.sendToNapplet(windowId, {
|
|
1095
|
+
type: "media.session.create.result",
|
|
1096
|
+
id: m.id ?? "",
|
|
1097
|
+
error: "missing owner"
|
|
1098
|
+
});
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (m.owner === "shell") {
|
|
1102
|
+
hooks.sendToNapplet(windowId, {
|
|
1103
|
+
type: "media.session.create.result",
|
|
1104
|
+
id: m.id ?? "",
|
|
1105
|
+
owner: "shell",
|
|
1106
|
+
error: "unsupported owner mode"
|
|
1107
|
+
});
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
hooks.sendToNapplet(windowId, {
|
|
1111
|
+
type: "media.session.create.result",
|
|
1112
|
+
id: m.id ?? "",
|
|
1113
|
+
sessionId: m.sessionId ?? "",
|
|
1114
|
+
owner: m.owner
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function handleKeysMessage(context, windowId, msg) {
|
|
1119
|
+
const { hooks, serviceRegistry } = context;
|
|
1120
|
+
const keysService = serviceRegistry["keys"];
|
|
1121
|
+
if (keysService) {
|
|
1122
|
+
keysService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (msg.type === "keys.forward") {
|
|
1126
|
+
forwardHotkey(hooks, msg);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (msg.type === "keys.registerAction") sendRegisterActionResult(hooks, windowId, msg);
|
|
1130
|
+
}
|
|
1131
|
+
function forwardHotkey(hooks, msg) {
|
|
1132
|
+
const m = msg;
|
|
1133
|
+
hooks.hotkeys.executeHotkeyFromForward({
|
|
1134
|
+
key: m.key ?? "",
|
|
1135
|
+
code: m.code ?? "",
|
|
1136
|
+
ctrlKey: !!m.ctrl,
|
|
1137
|
+
altKey: !!m.alt,
|
|
1138
|
+
shiftKey: !!m.shift,
|
|
1139
|
+
metaKey: !!m.meta
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
function sendRegisterActionResult(hooks, windowId, msg) {
|
|
1143
|
+
const m = msg;
|
|
1144
|
+
hooks.sendToNapplet(windowId, {
|
|
1145
|
+
type: "keys.registerAction.result",
|
|
1146
|
+
id: m.id ?? "",
|
|
1147
|
+
actionId: m.action?.id ?? "",
|
|
1148
|
+
...m.action?.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
function handleNotifyMessage(context, windowId, msg) {
|
|
1152
|
+
const { hooks, serviceRegistry } = context;
|
|
1153
|
+
const notifyService = serviceRegistry["notify"];
|
|
1154
|
+
if (notifyService) {
|
|
1155
|
+
notifyService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (msg.type === "notify.send") {
|
|
1159
|
+
const m = msg;
|
|
1160
|
+
hooks.sendToNapplet(windowId, { type: "notify.send.result", id: m.id ?? "", notificationId: `shell-${Date.now()}` });
|
|
1161
|
+
} else if (msg.type === "notify.permission.request") {
|
|
1162
|
+
const m = msg;
|
|
1163
|
+
hooks.sendToNapplet(windowId, { type: "notify.permission.result", id: m.id ?? "", granted: true });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function handleThemeMessage(context, windowId, msg) {
|
|
1167
|
+
const { hooks, serviceRegistry } = context;
|
|
1168
|
+
const themeService = serviceRegistry["theme"];
|
|
1169
|
+
if (themeService) {
|
|
1170
|
+
themeService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (msg.type === "theme.get") {
|
|
1174
|
+
const m = msg;
|
|
1175
|
+
hooks.sendToNapplet(windowId, {
|
|
1176
|
+
type: "theme.get.result",
|
|
1177
|
+
id: m.id ?? "",
|
|
1178
|
+
theme: THEME_FALLBACK_DEFAULT
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function handleServiceOnlyMessage(context, name, windowId, msg) {
|
|
1183
|
+
const service = context.serviceRegistry[name];
|
|
1184
|
+
if (!service) return;
|
|
1185
|
+
service.handleMessage(windowId, msg, (resp) => context.hooks.sendToNapplet(windowId, resp));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
536
1188
|
// src/runtime.ts
|
|
537
|
-
function
|
|
538
|
-
const subscriptions = /* @__PURE__ */ new Map();
|
|
539
|
-
const ifcSubscriptions = /* @__PURE__ */ new Map();
|
|
540
|
-
const ifcChannels = /* @__PURE__ */ new Map();
|
|
541
|
-
const ifcChannelsByWindow = /* @__PURE__ */ new Map();
|
|
542
|
-
let _consentHandler = null;
|
|
543
|
-
const serviceRegistry = { ...hooks.services ?? {} };
|
|
1189
|
+
function createRegisteredServices(serviceRegistry) {
|
|
544
1190
|
const registeredServices = /* @__PURE__ */ new Map();
|
|
545
1191
|
for (const [name, handler] of Object.entries(serviceRegistry)) {
|
|
546
1192
|
registeredServices.set(name, {
|
|
@@ -549,824 +1195,36 @@ function createRuntime(hooks) {
|
|
|
549
1195
|
description: handler.descriptor.description
|
|
550
1196
|
});
|
|
551
1197
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const manifestCache = createManifestCache(hooks.manifestPersistence);
|
|
556
|
-
const replayDetector = createReplayDetector(
|
|
557
|
-
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().replayWindowSeconds : void 0
|
|
558
|
-
);
|
|
559
|
-
const enforce = createEnforceGate({
|
|
560
|
-
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
561
|
-
resolveIdentity: (pubkey) => {
|
|
562
|
-
const entry = sessionRegistry.getEntry(pubkey);
|
|
563
|
-
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash } : void 0;
|
|
564
|
-
},
|
|
565
|
-
onAclCheck: hooks.onAclCheck
|
|
566
|
-
});
|
|
567
|
-
const enforceNub = createNubEnforceGate({
|
|
568
|
-
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
569
|
-
resolveIdentityByWindowId: (windowId) => {
|
|
570
|
-
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
571
|
-
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash } : void 0;
|
|
572
|
-
},
|
|
573
|
-
onAclCheck: hooks.onAclCheck
|
|
574
|
-
});
|
|
575
|
-
const eventBuffer = createEventBuffer(
|
|
576
|
-
hooks.sendToNapplet,
|
|
577
|
-
sessionRegistry,
|
|
578
|
-
enforce,
|
|
579
|
-
subscriptions,
|
|
580
|
-
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().ringBufferSize ?? RING_BUFFER_SIZE : void 0
|
|
581
|
-
);
|
|
582
|
-
aclState.load();
|
|
583
|
-
manifestCache.load();
|
|
584
|
-
function checkCompatibility(requires, windowId, eventId) {
|
|
585
|
-
if (requires.length === 0) return true;
|
|
586
|
-
const available = Array.from(registeredServices.values());
|
|
587
|
-
const registeredNames = new Set(registeredServices.keys());
|
|
588
|
-
const missing = requires.filter((name) => !registeredNames.has(name));
|
|
589
|
-
const compatible = missing.length === 0;
|
|
590
|
-
if (!compatible) {
|
|
591
|
-
const report = { available, missing, compatible };
|
|
592
|
-
hooks.onCompatibilityIssue?.(report);
|
|
593
|
-
if (hooks.strictMode) {
|
|
594
|
-
hooks.sendToNapplet(windowId, [
|
|
595
|
-
"OK",
|
|
596
|
-
eventId,
|
|
597
|
-
false,
|
|
598
|
-
`blocked: missing required services: ${missing.join(", ")}`
|
|
599
|
-
]);
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return true;
|
|
604
|
-
}
|
|
605
|
-
function checkUndeclaredService(windowId, pubkey, serviceName, event, onApproved) {
|
|
606
|
-
if (!registeredServices.has(serviceName)) return true;
|
|
607
|
-
const nappletPubkey = sessionRegistry.getPubkey(windowId);
|
|
608
|
-
if (!nappletPubkey) return true;
|
|
609
|
-
const nappletEntry = sessionRegistry.getEntry(nappletPubkey);
|
|
610
|
-
if (!nappletEntry) return true;
|
|
611
|
-
const requires = manifestCache.getRequires(nappletEntry.pubkey, nappletEntry.dTag);
|
|
612
|
-
if (requires.includes(serviceName)) return true;
|
|
613
|
-
const consentKey = `${windowId}:${serviceName}`;
|
|
614
|
-
if (undeclaredServiceConsents.has(consentKey)) return true;
|
|
615
|
-
if (_consentHandler) {
|
|
616
|
-
_consentHandler({
|
|
617
|
-
type: "undeclared-service",
|
|
618
|
-
windowId,
|
|
619
|
-
pubkey,
|
|
620
|
-
event,
|
|
621
|
-
serviceName,
|
|
622
|
-
resolve: (allowed) => {
|
|
623
|
-
if (allowed) {
|
|
624
|
-
undeclaredServiceConsents.add(consentKey);
|
|
625
|
-
onApproved();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
return false;
|
|
630
|
-
}
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
function handleHotkeyForward(event) {
|
|
634
|
-
const keyData = {
|
|
635
|
-
key: event.tags?.find((t) => t[0] === "key")?.[1] ?? "",
|
|
636
|
-
code: event.tags?.find((t) => t[0] === "code")?.[1] ?? "",
|
|
637
|
-
ctrlKey: event.tags?.find((t) => t[0] === "ctrl")?.[1] === "1",
|
|
638
|
-
altKey: event.tags?.find((t) => t[0] === "alt")?.[1] === "1",
|
|
639
|
-
shiftKey: event.tags?.find((t) => t[0] === "shift")?.[1] === "1",
|
|
640
|
-
metaKey: event.tags?.find((t) => t[0] === "meta")?.[1] === "1"
|
|
641
|
-
};
|
|
642
|
-
hooks.hotkeys.executeHotkeyFromForward(keyData);
|
|
643
|
-
}
|
|
644
|
-
function handleShellCommand(event, windowId, topic) {
|
|
645
|
-
function sendOk(success, reason) {
|
|
646
|
-
hooks.sendToNapplet(windowId, ["OK", event.id, success, reason]);
|
|
647
|
-
}
|
|
648
|
-
function sendInterPaneReply(replyTopic, content) {
|
|
649
|
-
const responseEvent = {
|
|
650
|
-
kind: 29e3,
|
|
651
|
-
// IPC_PEER — inlined numeric after Phase 24 shim deletion
|
|
652
|
-
pubkey: "",
|
|
653
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
654
|
-
tags: [["t", replyTopic]],
|
|
655
|
-
content,
|
|
656
|
-
id: "",
|
|
657
|
-
sig: ""
|
|
658
|
-
};
|
|
659
|
-
hooks.sendToNapplet(windowId, ["EVENT", "__shell__", responseEvent]);
|
|
660
|
-
sendOk(true, "");
|
|
661
|
-
}
|
|
662
|
-
switch (topic) {
|
|
663
|
-
case "shell:acl-get": {
|
|
664
|
-
const aclEntries = aclState.getAllEntries();
|
|
665
|
-
const nappletEntries = sessionRegistry.getAllEntries();
|
|
666
|
-
const nappletInfoMap = {};
|
|
667
|
-
for (const e of nappletEntries) nappletInfoMap[e.pubkey] = { type: e.type, registeredAt: e.registeredAt };
|
|
668
|
-
const merged = [...aclEntries];
|
|
669
|
-
for (const e of nappletEntries) {
|
|
670
|
-
if (!merged.find((a) => a.pubkey === e.pubkey)) {
|
|
671
|
-
merged.push({ pubkey: e.pubkey, capabilities: [...ALL_CAPABILITIES], blocked: false });
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
const display = merged.map((e) => ({
|
|
675
|
-
...e,
|
|
676
|
-
type: nappletInfoMap[e.pubkey]?.type ?? "unknown",
|
|
677
|
-
registeredAt: nappletInfoMap[e.pubkey]?.registeredAt ?? 0
|
|
678
|
-
}));
|
|
679
|
-
sendInterPaneReply("shell:acl-current", JSON.stringify({ entries: display }));
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
case "shell:acl-revoke":
|
|
683
|
-
case "shell:acl-grant":
|
|
684
|
-
case "shell:acl-block":
|
|
685
|
-
case "shell:acl-unblock": {
|
|
686
|
-
const pk = event.tags?.find((t) => t[0] === "pubkey")?.[1];
|
|
687
|
-
const cap = event.tags?.find((t) => t[0] === "cap")?.[1];
|
|
688
|
-
if (!pk) {
|
|
689
|
-
sendOk(false, "error: missing pubkey tag");
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
const ne = sessionRegistry.getEntry(pk);
|
|
693
|
-
if (topic === "shell:acl-revoke" && cap) aclState.revoke(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "", cap);
|
|
694
|
-
else if (topic === "shell:acl-grant" && cap) aclState.grant(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "", cap);
|
|
695
|
-
else if (topic === "shell:acl-block") aclState.block(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "");
|
|
696
|
-
else if (topic === "shell:acl-unblock") aclState.unblock(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "");
|
|
697
|
-
aclState.persist();
|
|
698
|
-
const ae = aclState.getEntry(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "");
|
|
699
|
-
sendInterPaneReply("shell:acl-current", JSON.stringify({ entries: ae ? [ae] : [] }));
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
case "shell:relay-get":
|
|
703
|
-
sendInterPaneReply("shell:relay-current", JSON.stringify(hooks.relayConfig.getRelayConfig()));
|
|
704
|
-
break;
|
|
705
|
-
case "shell:relay-add": {
|
|
706
|
-
const tier = event.tags?.find((t) => t[0] === "tier")?.[1];
|
|
707
|
-
const url = event.tags?.find((t) => t[0] === "url")?.[1];
|
|
708
|
-
if (tier && url) {
|
|
709
|
-
hooks.relayConfig.addRelay(tier, url);
|
|
710
|
-
sendInterPaneReply("shell:relay-current", JSON.stringify(hooks.relayConfig.getRelayConfig()));
|
|
711
|
-
} else sendOk(false, "error: missing tier/url");
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
case "shell:relay-remove": {
|
|
715
|
-
const tier = event.tags?.find((t) => t[0] === "tier")?.[1];
|
|
716
|
-
const url = event.tags?.find((t) => t[0] === "url")?.[1];
|
|
717
|
-
if (tier && url) {
|
|
718
|
-
hooks.relayConfig.removeRelay(tier, url);
|
|
719
|
-
sendInterPaneReply("shell:relay-current", JSON.stringify(hooks.relayConfig.getRelayConfig()));
|
|
720
|
-
} else sendOk(false, "error: missing tier/url");
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
case "shell:relay-nip66":
|
|
724
|
-
sendInterPaneReply("shell:relay-nip66-data", JSON.stringify(hooks.relayConfig.getNip66Suggestions()));
|
|
725
|
-
break;
|
|
726
|
-
case "shell:relay-scoped-connect": {
|
|
727
|
-
const url = event.tags?.find((t) => t[0] === "url")?.[1];
|
|
728
|
-
const subId = event.tags?.find((t) => t[0] === "sub-id")?.[1];
|
|
729
|
-
const filtersTag = event.tags?.find((t) => t[0] === "filters")?.[1];
|
|
730
|
-
if (!url || !subId || !filtersTag) {
|
|
731
|
-
sendOk(false, "error: missing tags");
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
try {
|
|
735
|
-
const filters = JSON.parse(filtersTag);
|
|
736
|
-
hooks.relayPool?.openScopedRelay(windowId, url, subId, filters, hooks.sendToNapplet);
|
|
737
|
-
sendOk(true, "");
|
|
738
|
-
} catch {
|
|
739
|
-
sendOk(false, "error: invalid filters");
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case "shell:relay-scoped-close":
|
|
744
|
-
hooks.relayPool?.closeScopedRelay(windowId);
|
|
745
|
-
sendOk(true, "");
|
|
746
|
-
break;
|
|
747
|
-
case "shell:relay-scoped-publish": {
|
|
748
|
-
const et = event.tags?.find((t) => t[0] === "event")?.[1];
|
|
749
|
-
if (!et) {
|
|
750
|
-
sendOk(false, "error: missing event tag");
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
try {
|
|
754
|
-
const signed = JSON.parse(et);
|
|
755
|
-
const ok = hooks.relayPool?.publishToScopedRelay(windowId, signed) ?? false;
|
|
756
|
-
sendOk(ok, ok ? "" : "error: no active scoped relay");
|
|
757
|
-
} catch {
|
|
758
|
-
sendOk(false, "error: invalid event JSON");
|
|
759
|
-
}
|
|
760
|
-
break;
|
|
761
|
-
}
|
|
762
|
-
case "shell:create-window": {
|
|
763
|
-
try {
|
|
764
|
-
const payload = JSON.parse(event.content);
|
|
765
|
-
if (!payload.title || !payload.class) {
|
|
766
|
-
sendOk(false, "error: requires title and class");
|
|
767
|
-
break;
|
|
768
|
-
}
|
|
769
|
-
const id = hooks.windowManager.createWindow({ title: payload.title, class: payload.class, iframeSrc: payload.iframeSrc });
|
|
770
|
-
sendOk(!!id, id ? "" : "error: window creation failed");
|
|
771
|
-
} catch {
|
|
772
|
-
sendOk(false, "error: invalid JSON");
|
|
773
|
-
}
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
case "shell:send-dm": {
|
|
777
|
-
if (hooks.dm) {
|
|
778
|
-
const corrId = event.tags?.find((t) => t[0] === "id")?.[1] ?? "";
|
|
779
|
-
const recipient = event.tags?.find((t) => t[0] === "p")?.[1];
|
|
780
|
-
let message;
|
|
781
|
-
try {
|
|
782
|
-
message = JSON.parse(event.content).message;
|
|
783
|
-
} catch {
|
|
784
|
-
}
|
|
785
|
-
if (!recipient || !message) {
|
|
786
|
-
sendOk(false, "error: missing recipient or message");
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
hooks.dm.sendDm(recipient, message).then((result) => {
|
|
790
|
-
const payload = result.success ? { success: true, ...result.eventId ? { eventId: result.eventId } : {} } : { success: false, error: result.error ?? "unknown error" };
|
|
791
|
-
const response = {
|
|
792
|
-
kind: 29e3,
|
|
793
|
-
// IPC_PEER — inlined numeric after Phase 24 shim deletion
|
|
794
|
-
pubkey: "",
|
|
795
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
796
|
-
tags: [["t", "shell:send-dm-result"], ["id", corrId]],
|
|
797
|
-
content: JSON.stringify(payload),
|
|
798
|
-
id: "",
|
|
799
|
-
sig: ""
|
|
800
|
-
};
|
|
801
|
-
hooks.sendToNapplet(windowId, ["EVENT", "__shell__", response]);
|
|
802
|
-
sendOk(result.success, result.success ? "" : `error: ${result.error}`);
|
|
803
|
-
}).catch(() => {
|
|
804
|
-
sendOk(false, "error: DM send failed");
|
|
805
|
-
});
|
|
806
|
-
} else sendOk(false, "error: DM hooks not configured");
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
default:
|
|
810
|
-
sendOk(true, "");
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
function handleRelayMessage(windowId, msg) {
|
|
815
|
-
const m = msg;
|
|
816
|
-
const dotIdx = msg.type.indexOf(".");
|
|
817
|
-
const action = msg.type.slice(dotIdx + 1);
|
|
818
|
-
switch (action) {
|
|
819
|
-
case "subscribe": {
|
|
820
|
-
let deliver2 = function(event) {
|
|
821
|
-
if (seenIds.has(event.id)) return;
|
|
822
|
-
seenIds.add(event.id);
|
|
823
|
-
if (subscriptions.has(subKey)) {
|
|
824
|
-
hooks.sendToNapplet(windowId, { type: "relay.event", subId, event });
|
|
825
|
-
}
|
|
826
|
-
};
|
|
827
|
-
var deliver = deliver2;
|
|
828
|
-
const subId = m.subId ?? "";
|
|
829
|
-
const filters = m.filters ?? [];
|
|
830
|
-
if (!subId) return;
|
|
831
|
-
const subKey = `${windowId}:${subId}`;
|
|
832
|
-
subscriptions.set(subKey, { windowId, filters });
|
|
833
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
834
|
-
for (const bufferedEvent of eventBuffer.getBufferedEvents()) {
|
|
835
|
-
if (matchesAnyFilter(bufferedEvent, filters)) deliver2(bufferedEvent);
|
|
836
|
-
}
|
|
837
|
-
const isShellKind = filters.length > 0 && filters.every((f) => f.kinds?.every((k) => k >= 29e3 && k < 3e4));
|
|
838
|
-
if (!isShellKind) {
|
|
839
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
840
|
-
const cacheService = !serviceRegistry["relay"] ? serviceRegistry["cache"] : void 0;
|
|
841
|
-
if (relayService) {
|
|
842
|
-
relayService.handleMessage(windowId, msg, (resp) => {
|
|
843
|
-
if (!subscriptions.has(subKey)) return;
|
|
844
|
-
hooks.sendToNapplet(windowId, resp);
|
|
845
|
-
});
|
|
846
|
-
if (cacheService) cacheService.handleMessage(windowId, msg, (resp) => {
|
|
847
|
-
if (!subscriptions.has(subKey)) return;
|
|
848
|
-
hooks.sendToNapplet(windowId, resp);
|
|
849
|
-
});
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
const cache = hooks.cache;
|
|
854
|
-
if (cache?.isAvailable() && !isShellKind) {
|
|
855
|
-
cache.query(filters).then((cachedEvents) => {
|
|
856
|
-
for (const event of cachedEvents) deliver2(event);
|
|
857
|
-
}).catch(() => {
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
const pool = hooks.relayPool;
|
|
861
|
-
if (pool?.isAvailable() && !isShellKind) {
|
|
862
|
-
const relayUrls = pool.selectRelayTier(filters);
|
|
863
|
-
let eoseSent = false;
|
|
864
|
-
const eoseFallbackTimer = setTimeout(() => {
|
|
865
|
-
if (!eoseSent) {
|
|
866
|
-
eoseSent = true;
|
|
867
|
-
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
868
|
-
}
|
|
869
|
-
}, 15e3);
|
|
870
|
-
const subscription = pool.subscribe(filters, (item) => {
|
|
871
|
-
if (item === "EOSE") {
|
|
872
|
-
clearTimeout(eoseFallbackTimer);
|
|
873
|
-
if (!eoseSent) {
|
|
874
|
-
eoseSent = true;
|
|
875
|
-
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
876
|
-
}
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
deliver2(item);
|
|
880
|
-
if (cache?.isAvailable() && !isShellKind) {
|
|
881
|
-
try {
|
|
882
|
-
cache.store(item);
|
|
883
|
-
} catch {
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}, relayUrls);
|
|
887
|
-
pool.trackSubscription(subKey, () => {
|
|
888
|
-
clearTimeout(eoseFallbackTimer);
|
|
889
|
-
subscription.unsubscribe();
|
|
890
|
-
});
|
|
891
|
-
} else if (!isShellKind) {
|
|
892
|
-
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
893
|
-
}
|
|
894
|
-
break;
|
|
895
|
-
}
|
|
896
|
-
case "close": {
|
|
897
|
-
const subId = m.subId ?? "";
|
|
898
|
-
if (!subId) return;
|
|
899
|
-
const subKey = `${windowId}:${subId}`;
|
|
900
|
-
subscriptions.delete(subKey);
|
|
901
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
902
|
-
if (relayService) {
|
|
903
|
-
relayService.handleMessage(windowId, msg, () => {
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
hooks.relayPool?.untrackSubscription(subKey);
|
|
907
|
-
hooks.sendToNapplet(windowId, { type: "relay.closed", subId, message: "" });
|
|
908
|
-
break;
|
|
909
|
-
}
|
|
910
|
-
case "publish": {
|
|
911
|
-
const event = m.event;
|
|
912
|
-
const id = m.id ?? "";
|
|
913
|
-
if (!event || typeof event !== "object") {
|
|
914
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.error", id, error: "invalid event" });
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const replayResult = replayDetector.check(event);
|
|
918
|
-
if (replayResult !== null) {
|
|
919
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: replayResult });
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
923
|
-
if (relayService) {
|
|
924
|
-
relayService.handleMessage(windowId, msg, (resp) => {
|
|
925
|
-
hooks.sendToNapplet(windowId, resp);
|
|
926
|
-
});
|
|
927
|
-
} else if (hooks.relayPool?.isAvailable()) {
|
|
928
|
-
hooks.relayPool.publish(event);
|
|
929
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: true });
|
|
930
|
-
} else {
|
|
931
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: "no relay pool available" });
|
|
932
|
-
}
|
|
933
|
-
eventBuffer.bufferAndDeliver(event, windowId);
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
// Shell-mediated encryption path (NUB-08 / SH-C03). Napplets hand the
|
|
937
|
-
// shell a plaintext EventTemplate plus a recipient pubkey; the shell
|
|
938
|
-
// encrypts via its own signer (nip44 default, nip04 opt-in), signs,
|
|
939
|
-
// publishes, and returns a relay.publishEncrypted.result. The signer's
|
|
940
|
-
// nip04/nip44 primitives are SHELL-INTERNAL — no napplet-visible
|
|
941
|
-
// message surface reaches them (SignerProxy was deleted in Plan 12-01).
|
|
942
|
-
case "publishEncrypted": {
|
|
943
|
-
let replyPe2 = function(ok, extra = {}) {
|
|
944
|
-
hooks.sendToNapplet(windowId, {
|
|
945
|
-
type: "relay.publishEncrypted.result",
|
|
946
|
-
id,
|
|
947
|
-
ok,
|
|
948
|
-
...extra
|
|
949
|
-
});
|
|
950
|
-
};
|
|
951
|
-
var replyPe = replyPe2;
|
|
952
|
-
const id = m.id ?? "";
|
|
953
|
-
const eventTemplate = m.event;
|
|
954
|
-
const peMsg = msg;
|
|
955
|
-
const recipient = peMsg.recipient ?? "";
|
|
956
|
-
const encryption = peMsg.encryption ?? "nip44";
|
|
957
|
-
if (!recipient) {
|
|
958
|
-
replyPe2(false, { error: "missing recipient" });
|
|
959
|
-
break;
|
|
960
|
-
}
|
|
961
|
-
if (encryption !== "nip44" && encryption !== "nip04") {
|
|
962
|
-
replyPe2(false, { error: `unsupported encryption scheme: ${encryption}` });
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
965
|
-
const peSigner = hooks.auth.getSigner();
|
|
966
|
-
if (!peSigner) {
|
|
967
|
-
replyPe2(false, { error: "no signer configured" });
|
|
968
|
-
break;
|
|
969
|
-
}
|
|
970
|
-
if (!eventTemplate || typeof eventTemplate !== "object") {
|
|
971
|
-
replyPe2(false, { error: "invalid event template" });
|
|
972
|
-
break;
|
|
973
|
-
}
|
|
974
|
-
(async () => {
|
|
975
|
-
try {
|
|
976
|
-
const plaintext = String(eventTemplate.content ?? "");
|
|
977
|
-
const ciphertext = encryption === "nip44" ? await peSigner.nip44?.encrypt(recipient, plaintext) ?? "" : await peSigner.nip04?.encrypt(recipient, plaintext) ?? "";
|
|
978
|
-
const eventWithCiphertext = { ...eventTemplate, content: ciphertext };
|
|
979
|
-
const signed = await peSigner.signEvent?.(eventWithCiphertext);
|
|
980
|
-
if (!signed) {
|
|
981
|
-
replyPe2(false, { error: "signEvent returned null" });
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
985
|
-
if (relayService) {
|
|
986
|
-
const publishMsg = { type: "relay.publish", id, event: signed };
|
|
987
|
-
let replied = false;
|
|
988
|
-
relayService.handleMessage(windowId, publishMsg, (resp) => {
|
|
989
|
-
if (replied) return;
|
|
990
|
-
const r = resp;
|
|
991
|
-
if (typeof r.type === "string" && r.type.startsWith("relay.publish")) {
|
|
992
|
-
const okVal = r.ok ?? r.accepted ?? false;
|
|
993
|
-
replied = true;
|
|
994
|
-
replyPe2(okVal, {
|
|
995
|
-
event: signed,
|
|
996
|
-
eventId: signed.id,
|
|
997
|
-
...okVal ? {} : { error: r.error ?? r.message ?? "publish failed" }
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
if (!replied) {
|
|
1002
|
-
replied = true;
|
|
1003
|
-
replyPe2(true, { event: signed, eventId: signed.id });
|
|
1004
|
-
}
|
|
1005
|
-
} else if (hooks.relayPool?.isAvailable()) {
|
|
1006
|
-
hooks.relayPool.publish(signed);
|
|
1007
|
-
replyPe2(true, { event: signed, eventId: signed.id });
|
|
1008
|
-
} else {
|
|
1009
|
-
replyPe2(false, { error: "no relay pool available" });
|
|
1010
|
-
}
|
|
1011
|
-
try {
|
|
1012
|
-
eventBuffer.bufferAndDeliver(signed, windowId);
|
|
1013
|
-
} catch {
|
|
1014
|
-
}
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
replyPe2(false, { error: err?.message ?? "encryption failed" });
|
|
1017
|
-
}
|
|
1018
|
-
})();
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
case "query": {
|
|
1022
|
-
const id = m.id ?? "";
|
|
1023
|
-
const filters = m.filters ?? [];
|
|
1024
|
-
let count = 0;
|
|
1025
|
-
for (const event of eventBuffer.getBufferedEvents()) {
|
|
1026
|
-
if (matchesAnyFilter(event, filters)) count++;
|
|
1027
|
-
}
|
|
1028
|
-
hooks.sendToNapplet(windowId, { type: "relay.query.result", id, count });
|
|
1029
|
-
break;
|
|
1030
|
-
}
|
|
1031
|
-
default:
|
|
1032
|
-
break;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
function handleIdentityMessage(windowId, msg) {
|
|
1036
|
-
const identityService = serviceRegistry["identity"];
|
|
1037
|
-
if (identityService) {
|
|
1038
|
-
identityService.handleMessage(windowId, msg, (resp) => {
|
|
1039
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1040
|
-
});
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
const id = msg.id ?? "";
|
|
1044
|
-
const action = msg.type.slice("identity.".length);
|
|
1045
|
-
const signer = hooks.auth.getSigner();
|
|
1046
|
-
function sendError(error) {
|
|
1047
|
-
hooks.sendToNapplet(windowId, { type: `${msg.type}.error`, id, error });
|
|
1048
|
-
}
|
|
1049
|
-
function sendResult(payload) {
|
|
1050
|
-
hooks.sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
1051
|
-
}
|
|
1052
|
-
switch (action) {
|
|
1053
|
-
case "getPublicKey": {
|
|
1054
|
-
if (!signer) {
|
|
1055
|
-
sendError("no signer configured");
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey })).catch((err) => sendError(err?.message ?? "getPublicKey failed"));
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
case "getRelays": {
|
|
1062
|
-
if (!signer) {
|
|
1063
|
-
sendError("no signer configured");
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch((err) => sendError(err?.message ?? "getRelays failed"));
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
case "getProfile":
|
|
1070
|
-
sendResult({ profile: null });
|
|
1071
|
-
return;
|
|
1072
|
-
case "getFollows":
|
|
1073
|
-
sendResult({ pubkeys: [] });
|
|
1074
|
-
return;
|
|
1075
|
-
case "getList":
|
|
1076
|
-
sendResult({ entries: [] });
|
|
1077
|
-
return;
|
|
1078
|
-
case "getZaps":
|
|
1079
|
-
sendResult({ zaps: [] });
|
|
1080
|
-
return;
|
|
1081
|
-
case "getMutes":
|
|
1082
|
-
sendResult({ pubkeys: [] });
|
|
1083
|
-
return;
|
|
1084
|
-
case "getBlocked":
|
|
1085
|
-
sendResult({ pubkeys: [] });
|
|
1086
|
-
return;
|
|
1087
|
-
case "getBadges":
|
|
1088
|
-
sendResult({ badges: [] });
|
|
1089
|
-
return;
|
|
1090
|
-
default:
|
|
1091
|
-
sendError(`Unknown identity action: ${action}`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
function handleStorageMessage(windowId, msg) {
|
|
1095
|
-
handleStorageNub(windowId, msg, hooks.sendToNapplet, sessionRegistry, aclState, hooks.statePersistence);
|
|
1096
|
-
}
|
|
1097
|
-
function ifcAddChannel(channelId, peerA, peerB) {
|
|
1098
|
-
ifcChannels.set(channelId, { channelId, peerA, peerB });
|
|
1099
|
-
for (const w of [peerA, peerB]) {
|
|
1100
|
-
let set = ifcChannelsByWindow.get(w);
|
|
1101
|
-
if (!set) {
|
|
1102
|
-
set = /* @__PURE__ */ new Set();
|
|
1103
|
-
ifcChannelsByWindow.set(w, set);
|
|
1104
|
-
}
|
|
1105
|
-
set.add(channelId);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
function ifcRemoveChannel(channelId) {
|
|
1109
|
-
const ch = ifcChannels.get(channelId);
|
|
1110
|
-
if (!ch) return;
|
|
1111
|
-
ifcChannels.delete(channelId);
|
|
1112
|
-
for (const w of [ch.peerA, ch.peerB]) {
|
|
1113
|
-
const set = ifcChannelsByWindow.get(w);
|
|
1114
|
-
if (set) {
|
|
1115
|
-
set.delete(channelId);
|
|
1116
|
-
if (set.size === 0) ifcChannelsByWindow.delete(w);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
function ifcPeerOf(channelId, self) {
|
|
1121
|
-
const ch = ifcChannels.get(channelId);
|
|
1122
|
-
if (!ch) return null;
|
|
1123
|
-
if (ch.peerA === self) return ch.peerB;
|
|
1124
|
-
if (ch.peerB === self) return ch.peerA;
|
|
1125
|
-
return null;
|
|
1126
|
-
}
|
|
1127
|
-
function ifcGenerateChannelId() {
|
|
1128
|
-
return hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
|
|
1129
|
-
}
|
|
1130
|
-
function ifcResolveTarget(target) {
|
|
1131
|
-
if (sessionRegistry.getEntryByWindowId(target)) return target;
|
|
1132
|
-
const entries = sessionRegistry.getAllEntries();
|
|
1133
|
-
const byPubkey = entries.find((e) => e.pubkey === target);
|
|
1134
|
-
return byPubkey?.windowId ?? null;
|
|
1135
|
-
}
|
|
1136
|
-
function handleIfcMessage(windowId, msg) {
|
|
1137
|
-
const m = msg;
|
|
1138
|
-
const dotIdx = msg.type.indexOf(".");
|
|
1139
|
-
const action = msg.type.slice(dotIdx + 1);
|
|
1140
|
-
switch (action) {
|
|
1141
|
-
case "emit": {
|
|
1142
|
-
const topic = m.topic ?? "";
|
|
1143
|
-
const payload = m.payload;
|
|
1144
|
-
if (!topic) return;
|
|
1145
|
-
const subscribers = ifcSubscriptions.get(topic);
|
|
1146
|
-
if (subscribers) {
|
|
1147
|
-
for (const subscriberWindowId of subscribers) {
|
|
1148
|
-
if (subscriberWindowId === windowId) continue;
|
|
1149
|
-
hooks.sendToNapplet(subscriberWindowId, { type: "ifc.event", topic, payload, sender: windowId });
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
case "subscribe": {
|
|
1155
|
-
const id = m.id ?? "";
|
|
1156
|
-
const topic = m.topic ?? "";
|
|
1157
|
-
if (!topic) {
|
|
1158
|
-
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id, error: "missing topic" });
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
let subs = ifcSubscriptions.get(topic);
|
|
1162
|
-
if (!subs) {
|
|
1163
|
-
subs = /* @__PURE__ */ new Set();
|
|
1164
|
-
ifcSubscriptions.set(topic, subs);
|
|
1165
|
-
}
|
|
1166
|
-
subs.add(windowId);
|
|
1167
|
-
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id });
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
case "unsubscribe": {
|
|
1171
|
-
const topic = m.topic ?? "";
|
|
1172
|
-
if (!topic) return;
|
|
1173
|
-
const subs = ifcSubscriptions.get(topic);
|
|
1174
|
-
if (subs) {
|
|
1175
|
-
subs.delete(windowId);
|
|
1176
|
-
if (subs.size === 0) ifcSubscriptions.delete(topic);
|
|
1177
|
-
}
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
case "channel.open": {
|
|
1181
|
-
const id = m.id ?? "";
|
|
1182
|
-
const target = m.target ?? "";
|
|
1183
|
-
const peerWindow = ifcResolveTarget(target);
|
|
1184
|
-
if (!peerWindow) {
|
|
1185
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, error: "target not found" });
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
const channelId = ifcGenerateChannelId();
|
|
1189
|
-
ifcAddChannel(channelId, windowId, peerWindow);
|
|
1190
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, channelId, peer: peerWindow });
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
case "channel.emit": {
|
|
1194
|
-
const channelId = m.channelId ?? "";
|
|
1195
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1196
|
-
if (!peer) return;
|
|
1197
|
-
hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
case "channel.broadcast": {
|
|
1201
|
-
const channels = ifcChannelsByWindow.get(windowId);
|
|
1202
|
-
if (!channels) return;
|
|
1203
|
-
for (const channelId of channels) {
|
|
1204
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1205
|
-
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
|
|
1206
|
-
}
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
case "channel.list": {
|
|
1210
|
-
const id = m.id ?? "";
|
|
1211
|
-
const channels = [];
|
|
1212
|
-
const set = ifcChannelsByWindow.get(windowId);
|
|
1213
|
-
if (set) {
|
|
1214
|
-
for (const channelId of set) {
|
|
1215
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1216
|
-
if (peer) channels.push({ id: channelId, peer });
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.list.result", id, channels });
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
case "channel.close": {
|
|
1223
|
-
const channelId = m.channelId ?? "";
|
|
1224
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1225
|
-
if (!peer) return;
|
|
1226
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.closed", channelId });
|
|
1227
|
-
hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
1228
|
-
ifcRemoveChannel(channelId);
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
default:
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
function handleMediaMessage(windowId, msg) {
|
|
1236
|
-
const mediaService = serviceRegistry["media"];
|
|
1237
|
-
if (mediaService) {
|
|
1238
|
-
mediaService.handleMessage(windowId, msg, (resp) => {
|
|
1239
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1240
|
-
});
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if (msg.type === "media.session.create") {
|
|
1244
|
-
const m = msg;
|
|
1245
|
-
hooks.sendToNapplet(windowId, {
|
|
1246
|
-
type: "media.session.create.result",
|
|
1247
|
-
id: m.id ?? "",
|
|
1248
|
-
sessionId: m.sessionId ?? ""
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
function handleKeysMessage(windowId, msg) {
|
|
1253
|
-
const keysService = serviceRegistry["keys"];
|
|
1254
|
-
if (keysService) {
|
|
1255
|
-
keysService.handleMessage(windowId, msg, (resp) => {
|
|
1256
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1257
|
-
});
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
if (msg.type === "keys.forward") {
|
|
1261
|
-
const m = msg;
|
|
1262
|
-
hooks.hotkeys.executeHotkeyFromForward({
|
|
1263
|
-
key: m.key ?? "",
|
|
1264
|
-
code: m.code ?? "",
|
|
1265
|
-
ctrlKey: !!m.ctrl,
|
|
1266
|
-
altKey: !!m.alt,
|
|
1267
|
-
shiftKey: !!m.shift,
|
|
1268
|
-
metaKey: !!m.meta
|
|
1269
|
-
});
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
if (msg.type === "keys.registerAction") {
|
|
1273
|
-
const m = msg;
|
|
1274
|
-
hooks.sendToNapplet(windowId, {
|
|
1275
|
-
type: "keys.registerAction.result",
|
|
1276
|
-
id: m.id ?? "",
|
|
1277
|
-
actionId: m.action?.id ?? "",
|
|
1278
|
-
...m.action?.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
1279
|
-
});
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
function handleNotifyMessage(windowId, msg) {
|
|
1284
|
-
const notifyService = serviceRegistry["notify"];
|
|
1285
|
-
if (notifyService) {
|
|
1286
|
-
notifyService.handleMessage(windowId, msg, (resp) => {
|
|
1287
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1288
|
-
});
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
if (msg.type === "notify.send") {
|
|
1292
|
-
const m = msg;
|
|
1293
|
-
hooks.sendToNapplet(windowId, {
|
|
1294
|
-
type: "notify.send.result",
|
|
1295
|
-
id: m.id ?? "",
|
|
1296
|
-
notificationId: `shell-${Date.now()}`
|
|
1297
|
-
});
|
|
1298
|
-
} else if (msg.type === "notify.permission.request") {
|
|
1299
|
-
const m = msg;
|
|
1300
|
-
hooks.sendToNapplet(windowId, {
|
|
1301
|
-
type: "notify.permission.result",
|
|
1302
|
-
id: m.id ?? "",
|
|
1303
|
-
granted: true
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
const THEME_FALLBACK_DEFAULT = {
|
|
1308
|
-
colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
|
|
1309
|
-
};
|
|
1310
|
-
function handleThemeMessage(windowId, msg) {
|
|
1311
|
-
const themeService = serviceRegistry["theme"];
|
|
1312
|
-
if (themeService) {
|
|
1313
|
-
themeService.handleMessage(windowId, msg, (resp) => {
|
|
1314
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1315
|
-
});
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
if (msg.type === "theme.get") {
|
|
1319
|
-
const m = msg;
|
|
1320
|
-
hooks.sendToNapplet(windowId, {
|
|
1321
|
-
type: "theme.get.result",
|
|
1322
|
-
id: m.id ?? "",
|
|
1323
|
-
theme: THEME_FALLBACK_DEFAULT
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1198
|
+
return registeredServices;
|
|
1199
|
+
}
|
|
1200
|
+
function createNubEnvelopeDispatcher(handlers) {
|
|
1327
1201
|
let currentWindowId = null;
|
|
1328
1202
|
const nubDispatch = createDispatch();
|
|
1329
|
-
const
|
|
1330
|
-
if (currentWindowId
|
|
1331
|
-
handleRelayMessage(currentWindowId, msg);
|
|
1332
|
-
};
|
|
1333
|
-
const identityAdapter = (msg) => {
|
|
1334
|
-
if (currentWindowId === null) return;
|
|
1335
|
-
handleIdentityMessage(currentWindowId, msg);
|
|
1336
|
-
};
|
|
1337
|
-
const keysAdapter = (msg) => {
|
|
1338
|
-
if (currentWindowId === null) return;
|
|
1339
|
-
handleKeysMessage(currentWindowId, msg);
|
|
1340
|
-
};
|
|
1341
|
-
const mediaAdapter = (msg) => {
|
|
1342
|
-
if (currentWindowId === null) return;
|
|
1343
|
-
handleMediaMessage(currentWindowId, msg);
|
|
1344
|
-
};
|
|
1345
|
-
const notifyAdapter = (msg) => {
|
|
1346
|
-
if (currentWindowId === null) return;
|
|
1347
|
-
handleNotifyMessage(currentWindowId, msg);
|
|
1348
|
-
};
|
|
1349
|
-
const storageAdapter = (msg) => {
|
|
1350
|
-
if (currentWindowId === null) return;
|
|
1351
|
-
handleStorageMessage(currentWindowId, msg);
|
|
1352
|
-
};
|
|
1353
|
-
const ifcAdapter = (msg) => {
|
|
1354
|
-
if (currentWindowId === null) return;
|
|
1355
|
-
handleIfcMessage(currentWindowId, msg);
|
|
1203
|
+
const adapt = (handler) => (msg) => {
|
|
1204
|
+
if (currentWindowId !== null) handler(currentWindowId, msg);
|
|
1356
1205
|
};
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1206
|
+
nubDispatch.registerNub("relay", adapt(handlers.relay));
|
|
1207
|
+
nubDispatch.registerNub("identity", adapt(handlers.identity));
|
|
1208
|
+
nubDispatch.registerNub("keys", adapt(handlers.keys));
|
|
1209
|
+
nubDispatch.registerNub("media", adapt(handlers.media));
|
|
1210
|
+
nubDispatch.registerNub("notify", adapt(handlers.notify));
|
|
1211
|
+
nubDispatch.registerNub("storage", adapt(handlers.storage));
|
|
1212
|
+
nubDispatch.registerNub("ifc", adapt(handlers.ifc));
|
|
1213
|
+
nubDispatch.registerNub("theme", adapt(handlers.theme));
|
|
1214
|
+
nubDispatch.registerNub("config", adapt(handlers.config));
|
|
1215
|
+
nubDispatch.registerNub("resource", adapt(handlers.resource));
|
|
1216
|
+
nubDispatch.registerNub("cvm", adapt(handlers.cvm));
|
|
1217
|
+
return (windowId, envelope) => {
|
|
1218
|
+
currentWindowId = windowId;
|
|
1219
|
+
try {
|
|
1220
|
+
nubDispatch.dispatch(envelope);
|
|
1221
|
+
} finally {
|
|
1222
|
+
currentWindowId = null;
|
|
1223
|
+
}
|
|
1360
1224
|
};
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
nubDispatch.registerNub("media", mediaAdapter);
|
|
1365
|
-
nubDispatch.registerNub("notify", notifyAdapter);
|
|
1366
|
-
nubDispatch.registerNub("storage", storageAdapter);
|
|
1367
|
-
nubDispatch.registerNub("ifc", ifcAdapter);
|
|
1368
|
-
nubDispatch.registerNub("theme", themeAdapter);
|
|
1369
|
-
function handleMessage(windowId, msg) {
|
|
1225
|
+
}
|
|
1226
|
+
function createMessageHandler(hooks, enforceNub, dispatchNubEnvelope) {
|
|
1227
|
+
return (windowId, msg) => {
|
|
1370
1228
|
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
|
|
1371
1229
|
const envelope = msg;
|
|
1372
1230
|
const dotIdx = envelope.type.indexOf(".");
|
|
@@ -1376,47 +1234,62 @@ function createRuntime(hooks) {
|
|
|
1376
1234
|
const result = enforceNub(windowId, caps.senderCap, envelope);
|
|
1377
1235
|
if (!result.allowed) {
|
|
1378
1236
|
const id = envelope.id ?? "";
|
|
1379
|
-
|
|
1237
|
+
const isIdentityDecrypt = envelope.type === "identity.decrypt";
|
|
1238
|
+
const isStorageEnvelope = envelope.type.startsWith("storage.");
|
|
1239
|
+
const error = isIdentityDecrypt ? result.reason === "class-forbidden" ? "class-forbidden" : "policy-denied" : formatDenialReason(result.capability);
|
|
1240
|
+
const type = isStorageEnvelope ? `${envelope.type}.result` : `${envelope.type}.error`;
|
|
1241
|
+
hooks.sendToNapplet(windowId, { type, id, error });
|
|
1380
1242
|
return;
|
|
1381
1243
|
}
|
|
1382
1244
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1245
|
+
dispatchNubEnvelope(windowId, envelope);
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function createInjectedEvent(hooks, topic, payload) {
|
|
1249
|
+
const uuid = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 64).padEnd(64, "0");
|
|
1250
|
+
return {
|
|
1251
|
+
id: uuid,
|
|
1252
|
+
pubkey: "0".repeat(64),
|
|
1253
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1254
|
+
kind: 29e3,
|
|
1255
|
+
tags: [["t", topic]],
|
|
1256
|
+
content: JSON.stringify(payload),
|
|
1257
|
+
sig: "0".repeat(128)
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function createRuntimeInstance(context) {
|
|
1261
|
+
const {
|
|
1262
|
+
aclState,
|
|
1263
|
+
eventBuffer,
|
|
1264
|
+
hooks,
|
|
1265
|
+
ifcRuntime,
|
|
1266
|
+
manifestCache,
|
|
1267
|
+
registeredServices,
|
|
1268
|
+
replayDetector,
|
|
1269
|
+
serviceRegistry,
|
|
1270
|
+
sessionRegistry,
|
|
1271
|
+
subscriptions
|
|
1272
|
+
} = context;
|
|
1273
|
+
const undeclaredServiceConsents = /* @__PURE__ */ new Set();
|
|
1274
|
+
let consentHandler = null;
|
|
1275
|
+
return {
|
|
1276
|
+
handleMessage: context.handleMessage,
|
|
1392
1277
|
injectEvent(topic, payload) {
|
|
1393
|
-
|
|
1394
|
-
const event = {
|
|
1395
|
-
id: uuid,
|
|
1396
|
-
pubkey: "0".repeat(64),
|
|
1397
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
1398
|
-
kind: 29e3,
|
|
1399
|
-
// IPC_PEER — inlined numeric after Phase 24 shim deletion
|
|
1400
|
-
tags: [["t", topic]],
|
|
1401
|
-
content: JSON.stringify(payload),
|
|
1402
|
-
sig: "0".repeat(128)
|
|
1403
|
-
};
|
|
1404
|
-
eventBuffer.bufferAndDeliver(event, null);
|
|
1278
|
+
eventBuffer.bufferAndDeliver(createInjectedEvent(hooks, topic, payload), null);
|
|
1405
1279
|
},
|
|
1406
1280
|
destroy() {
|
|
1407
1281
|
manifestCache.persist();
|
|
1408
1282
|
aclState.persist();
|
|
1409
1283
|
replayDetector.clear();
|
|
1410
1284
|
subscriptions.clear();
|
|
1411
|
-
|
|
1412
|
-
ifcChannels.clear();
|
|
1413
|
-
ifcChannelsByWindow.clear();
|
|
1285
|
+
ifcRuntime.clear();
|
|
1414
1286
|
eventBuffer.clear();
|
|
1415
1287
|
registeredServices.clear();
|
|
1416
1288
|
undeclaredServiceConsents.clear();
|
|
1417
1289
|
},
|
|
1418
1290
|
registerConsentHandler(handler) {
|
|
1419
|
-
|
|
1291
|
+
consentHandler = handler;
|
|
1292
|
+
void consentHandler;
|
|
1420
1293
|
},
|
|
1421
1294
|
registerService(name, handler) {
|
|
1422
1295
|
serviceRegistry[name] = handler;
|
|
@@ -1437,20 +1310,7 @@ function createRuntime(hooks) {
|
|
|
1437
1310
|
hooks.relayPool?.untrackSubscription(key);
|
|
1438
1311
|
}
|
|
1439
1312
|
}
|
|
1440
|
-
|
|
1441
|
-
subs.delete(windowId);
|
|
1442
|
-
if (subs.size === 0) ifcSubscriptions.delete(topic);
|
|
1443
|
-
}
|
|
1444
|
-
const channelIds = ifcChannelsByWindow.get(windowId);
|
|
1445
|
-
if (channelIds) {
|
|
1446
|
-
for (const channelId of [...channelIds]) {
|
|
1447
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1448
|
-
if (peer) {
|
|
1449
|
-
hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
1450
|
-
}
|
|
1451
|
-
ifcRemoveChannel(channelId);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1313
|
+
ifcRuntime.destroyWindow(windowId);
|
|
1454
1314
|
notifyServiceWindowDestroyed(windowId, serviceRegistry);
|
|
1455
1315
|
},
|
|
1456
1316
|
get sessionRegistry() {
|
|
@@ -1463,7 +1323,64 @@ function createRuntime(hooks) {
|
|
|
1463
1323
|
return manifestCache;
|
|
1464
1324
|
}
|
|
1465
1325
|
};
|
|
1466
|
-
|
|
1326
|
+
}
|
|
1327
|
+
function createRuntime(hooks) {
|
|
1328
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
1329
|
+
const serviceRegistry = { ...hooks.services };
|
|
1330
|
+
const registeredServices = createRegisteredServices(serviceRegistry);
|
|
1331
|
+
const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate);
|
|
1332
|
+
const aclState = createAclState(hooks.aclPersistence);
|
|
1333
|
+
const manifestCache = createManifestCache(hooks.manifestPersistence);
|
|
1334
|
+
const replayDetector = createReplayDetector(
|
|
1335
|
+
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().replayWindowSeconds : void 0
|
|
1336
|
+
);
|
|
1337
|
+
const enforce = createEnforceGate({
|
|
1338
|
+
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
1339
|
+
resolveIdentity: (pubkey) => {
|
|
1340
|
+
const entry = sessionRegistry.getEntry(pubkey);
|
|
1341
|
+
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash } : void 0;
|
|
1342
|
+
},
|
|
1343
|
+
onAclCheck: hooks.onAclCheck
|
|
1344
|
+
});
|
|
1345
|
+
const enforceNub = createNubEnforceGate({
|
|
1346
|
+
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
1347
|
+
resolveIdentityByWindowId: (windowId) => {
|
|
1348
|
+
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
1349
|
+
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash, class: entry.class } : void 0;
|
|
1350
|
+
},
|
|
1351
|
+
onAclCheck: hooks.onAclCheck
|
|
1352
|
+
});
|
|
1353
|
+
const eventBuffer = createEventBuffer(
|
|
1354
|
+
hooks.sendToNapplet,
|
|
1355
|
+
sessionRegistry,
|
|
1356
|
+
enforce,
|
|
1357
|
+
subscriptions,
|
|
1358
|
+
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().ringBufferSize ?? RING_BUFFER_SIZE : void 0
|
|
1359
|
+
);
|
|
1360
|
+
aclState.load();
|
|
1361
|
+
manifestCache.load();
|
|
1362
|
+
const ifcRuntime = createIfcRuntime(hooks, sessionRegistry);
|
|
1363
|
+
const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry, aclState });
|
|
1364
|
+
const dispatchNubEnvelope = createNubEnvelopeDispatcher({
|
|
1365
|
+
relay: createRelayHandler({ hooks, serviceRegistry, subscriptions, eventBuffer, replayDetector }),
|
|
1366
|
+
identity: createIdentityHandler({ hooks, serviceRegistry }),
|
|
1367
|
+
ifc: ifcRuntime.handleMessage,
|
|
1368
|
+
...domainHandlers
|
|
1369
|
+
});
|
|
1370
|
+
const handleMessage = createMessageHandler(hooks, enforceNub, dispatchNubEnvelope);
|
|
1371
|
+
return createRuntimeInstance({
|
|
1372
|
+
hooks,
|
|
1373
|
+
serviceRegistry,
|
|
1374
|
+
registeredServices,
|
|
1375
|
+
replayDetector,
|
|
1376
|
+
subscriptions,
|
|
1377
|
+
eventBuffer,
|
|
1378
|
+
ifcRuntime,
|
|
1379
|
+
sessionRegistry,
|
|
1380
|
+
aclState,
|
|
1381
|
+
manifestCache,
|
|
1382
|
+
handleMessage
|
|
1383
|
+
});
|
|
1467
1384
|
}
|
|
1468
1385
|
|
|
1469
1386
|
// src/index.ts
|