@kehto/runtime 0.18.3 → 0.19.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 +110 -1
- package/dist/index.d.ts +115 -13
- package/dist/index.js +433 -142
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -66,11 +66,20 @@ function createSessionRegistry(notifier) {
|
|
|
66
66
|
getWindowId(pubkey) {
|
|
67
67
|
return byPubkey.get(pubkey)?.windowId;
|
|
68
68
|
},
|
|
69
|
+
getWindowIdByDTag(dTag) {
|
|
70
|
+
let resolvedWindowId;
|
|
71
|
+
for (const [windowId, entry] of byWindowIdEntry) {
|
|
72
|
+
if (entry.dTag !== dTag) continue;
|
|
73
|
+
if (resolvedWindowId !== void 0) return void 0;
|
|
74
|
+
resolvedWindowId = windowId;
|
|
75
|
+
}
|
|
76
|
+
return resolvedWindowId;
|
|
77
|
+
},
|
|
69
78
|
isRegistered(windowId) {
|
|
70
79
|
return byWindowId.has(windowId);
|
|
71
80
|
},
|
|
72
81
|
getAllEntries() {
|
|
73
|
-
return Array.from(
|
|
82
|
+
return Array.from(byWindowIdEntry.values());
|
|
74
83
|
},
|
|
75
84
|
getInstanceId(windowId) {
|
|
76
85
|
const pubkey = byWindowId.get(windowId);
|
|
@@ -178,7 +187,7 @@ function bitsToCapabilities(bits) {
|
|
|
178
187
|
function toIdentity(pubkey, dTag, hash) {
|
|
179
188
|
return { pubkey, dTag, hash };
|
|
180
189
|
}
|
|
181
|
-
function createAclState(persistence, defaultPolicy = "permissive") {
|
|
190
|
+
function createAclState(persistence, defaultPolicy = "permissive", onMutation) {
|
|
182
191
|
let state = createState(defaultPolicy);
|
|
183
192
|
function ensureRuntimeDefaultEntry(id) {
|
|
184
193
|
if (state.defaultPolicy !== "permissive") return;
|
|
@@ -199,11 +208,13 @@ function createAclState(persistence, defaultPolicy = "permissive") {
|
|
|
199
208
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
200
209
|
ensureRuntimeDefaultEntry(id);
|
|
201
210
|
state = revoke(state, id, capToBit(capability));
|
|
211
|
+
onMutation?.({ type: "revoke", dTag, aggregateHash, capability });
|
|
202
212
|
},
|
|
203
213
|
block(pubkey, dTag, aggregateHash) {
|
|
204
214
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
205
215
|
ensureRuntimeDefaultEntry(id);
|
|
206
216
|
state = block(state, id);
|
|
217
|
+
onMutation?.({ type: "block", dTag, aggregateHash });
|
|
207
218
|
},
|
|
208
219
|
unblock(pubkey, dTag, aggregateHash) {
|
|
209
220
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
@@ -399,18 +410,39 @@ function createManifestCache(persistence) {
|
|
|
399
410
|
var REPLAY_WINDOW_SECONDS = 30;
|
|
400
411
|
function createReplayDetector(getReplayWindow) {
|
|
401
412
|
const seenEventIds = /* @__PURE__ */ new Map();
|
|
413
|
+
function checkAndTrack(event, state) {
|
|
414
|
+
const replayWindow = getReplayWindow?.() ?? REPLAY_WINDOW_SECONDS;
|
|
415
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
416
|
+
for (const [id, entry] of seenEventIds) {
|
|
417
|
+
if (entry.state === "committed" && now - entry.timestamp > replayWindow) {
|
|
418
|
+
seenEventIds.delete(id);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (now - event.created_at > replayWindow) return "invalid: event created_at too old";
|
|
422
|
+
if (event.created_at - now > 10) return "invalid: event created_at in the future";
|
|
423
|
+
if (seenEventIds.has(event.id)) return "duplicate: already processed";
|
|
424
|
+
seenEventIds.set(event.id, { state, timestamp: now });
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
402
427
|
return {
|
|
403
428
|
check(event) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
429
|
+
return checkAndTrack(event, "committed");
|
|
430
|
+
},
|
|
431
|
+
reserve(event) {
|
|
432
|
+
return checkAndTrack(event, "pending");
|
|
433
|
+
},
|
|
434
|
+
commit(eventId) {
|
|
435
|
+
const entry = seenEventIds.get(eventId);
|
|
436
|
+
if (entry?.state !== "pending") return;
|
|
437
|
+
seenEventIds.set(eventId, {
|
|
438
|
+
state: "committed",
|
|
439
|
+
timestamp: Math.floor(Date.now() / 1e3)
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
release(eventId) {
|
|
443
|
+
if (seenEventIds.get(eventId)?.state === "pending") {
|
|
444
|
+
seenEventIds.delete(eventId);
|
|
412
445
|
}
|
|
413
|
-
return null;
|
|
414
446
|
},
|
|
415
447
|
clear() {
|
|
416
448
|
seenEventIds.clear();
|
|
@@ -524,21 +556,13 @@ import { createDispatch } from "@napplet/core";
|
|
|
524
556
|
|
|
525
557
|
// src/service-dispatch.ts
|
|
526
558
|
function routeServiceMessage(windowId, message, services, sendToNapplet) {
|
|
559
|
+
if (message.type === "inc.emit") return false;
|
|
527
560
|
const domain = message.type.split(".")[0];
|
|
528
561
|
const handler = services[domain];
|
|
529
562
|
if (handler) {
|
|
530
563
|
handler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
531
564
|
return true;
|
|
532
565
|
}
|
|
533
|
-
const incMessage = message;
|
|
534
|
-
if (message.type === "inc.emit" && typeof incMessage.topic === "string") {
|
|
535
|
-
const prefix = incMessage.topic.split(":")[0];
|
|
536
|
-
const incHandler = services[prefix];
|
|
537
|
-
if (incHandler) {
|
|
538
|
-
incHandler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
539
|
-
return true;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
566
|
return false;
|
|
543
567
|
}
|
|
544
568
|
function notifyServiceWindowDestroyed(windowId, services) {
|
|
@@ -564,7 +588,7 @@ function createRelayHandler(context) {
|
|
|
564
588
|
handleRelayClose(context, windowId, msg, m);
|
|
565
589
|
return;
|
|
566
590
|
case "publish":
|
|
567
|
-
handleRelayPublish(context, windowId,
|
|
591
|
+
handleRelayPublish(context, windowId, m);
|
|
568
592
|
return;
|
|
569
593
|
case "publishEncrypted":
|
|
570
594
|
handleRelayPublishEncrypted(context, windowId, msg);
|
|
@@ -688,29 +712,106 @@ function handleRelayClose(context, windowId, msg, m) {
|
|
|
688
712
|
hooks.relayPool?.untrackSubscription(subKey);
|
|
689
713
|
hooks.sendToNapplet(windowId, { type: "relay.closed", subId, message: "" });
|
|
690
714
|
}
|
|
691
|
-
function handleRelayPublish(context, windowId,
|
|
692
|
-
const {
|
|
693
|
-
const
|
|
715
|
+
function handleRelayPublish(context, windowId, m) {
|
|
716
|
+
const { hooks, replayDetector } = context;
|
|
717
|
+
const eventTemplate = m.event;
|
|
694
718
|
const id = m.id ?? "";
|
|
695
|
-
if (!
|
|
696
|
-
hooks
|
|
719
|
+
if (!eventTemplate || typeof eventTemplate !== "object") {
|
|
720
|
+
sendRelayPublishResult(hooks, windowId, id, false, void 0, "invalid event template");
|
|
697
721
|
return;
|
|
698
722
|
}
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
723
|
+
const signer = hooks.auth.getSigner();
|
|
724
|
+
const signEvent = signer?.signEvent?.bind(signer);
|
|
725
|
+
if (!signEvent) {
|
|
726
|
+
sendRelayPublishResult(hooks, windowId, id, false, void 0, "no signer configured");
|
|
702
727
|
return;
|
|
703
728
|
}
|
|
729
|
+
void (async () => {
|
|
730
|
+
try {
|
|
731
|
+
const signed = await signEvent(eventTemplate);
|
|
732
|
+
if (!signed) {
|
|
733
|
+
sendRelayPublishResult(hooks, windowId, id, false, void 0, "signEvent returned no event");
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const replayResult = replayDetector.reserve(signed);
|
|
737
|
+
if (replayResult !== null) {
|
|
738
|
+
sendRelayPublishResult(hooks, windowId, id, false, void 0, replayResult);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
publishSignedRelayEvent(context, windowId, id, signed, (ok, error) => {
|
|
742
|
+
if (ok) replayDetector.commit(signed.id);
|
|
743
|
+
else replayDetector.release(signed.id);
|
|
744
|
+
sendRelayPublishResult(hooks, windowId, id, ok, ok ? signed : void 0, error);
|
|
745
|
+
});
|
|
746
|
+
} catch (error) {
|
|
747
|
+
sendRelayPublishResult(
|
|
748
|
+
hooks,
|
|
749
|
+
windowId,
|
|
750
|
+
id,
|
|
751
|
+
false,
|
|
752
|
+
void 0,
|
|
753
|
+
error instanceof Error && error.message ? error.message : "event signing failed"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
}
|
|
758
|
+
function sendRelayPublishResult(hooks, windowId, id, ok, event, error) {
|
|
759
|
+
const result = ok && event ? { type: "relay.publish.result", id, ok: true, event, eventId: event.id } : { type: "relay.publish.result", id, ok: false, error: error ?? "publish failed" };
|
|
760
|
+
hooks.sendToNapplet(windowId, result);
|
|
761
|
+
}
|
|
762
|
+
function publishSignedRelayEvent(context, windowId, id, signed, reply) {
|
|
763
|
+
const { eventBuffer, hooks } = context;
|
|
764
|
+
let settled = false;
|
|
765
|
+
const settle = (ok, error) => {
|
|
766
|
+
if (settled) return;
|
|
767
|
+
settled = true;
|
|
768
|
+
if (ok) {
|
|
769
|
+
try {
|
|
770
|
+
eventBuffer.bufferAndDeliver(signed, windowId);
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
reply(ok, error);
|
|
775
|
+
};
|
|
704
776
|
const relayService = relayServiceFrom(context);
|
|
705
777
|
if (relayService) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
778
|
+
const publishMessage = { type: "relay.publish", id, event: signed };
|
|
779
|
+
try {
|
|
780
|
+
relayService.handleMessage(windowId, publishMessage, (response) => {
|
|
781
|
+
if (response.type !== "relay.publish.result" && response.type !== "relay.publish.error") {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const result = response;
|
|
785
|
+
const ok = response.type === "relay.publish.result" && (result.ok ?? result.accepted ?? false);
|
|
786
|
+
settle(ok, ok ? void 0 : result.error ?? result.message ?? "publish failed");
|
|
787
|
+
});
|
|
788
|
+
} catch (error) {
|
|
789
|
+
settle(
|
|
790
|
+
false,
|
|
791
|
+
error instanceof Error && error.message ? error.message : "relay service failed"
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (!hooks.relayPool?.isAvailable()) {
|
|
797
|
+
settle(false, "no relay pool available");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
const publishResult = hooks.relayPool.publish(signed);
|
|
802
|
+
void Promise.resolve(publishResult).then(
|
|
803
|
+
() => settle(true),
|
|
804
|
+
(error) => settle(
|
|
805
|
+
false,
|
|
806
|
+
error instanceof Error && error.message ? error.message : "publish failed"
|
|
807
|
+
)
|
|
808
|
+
);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
settle(
|
|
811
|
+
false,
|
|
812
|
+
error instanceof Error && error.message ? error.message : "publish failed"
|
|
813
|
+
);
|
|
712
814
|
}
|
|
713
|
-
eventBuffer.bufferAndDeliver(event, windowId);
|
|
714
815
|
}
|
|
715
816
|
function handleRelayPublishEncrypted(context, windowId, msg) {
|
|
716
817
|
const { hooks } = context;
|
|
@@ -742,7 +843,7 @@ function handleRelayPublishEncrypted(context, windowId, msg) {
|
|
|
742
843
|
publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe);
|
|
743
844
|
}
|
|
744
845
|
function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe) {
|
|
745
|
-
const {
|
|
846
|
+
const { hooks } = context;
|
|
746
847
|
const peSigner = hooks.auth.getSigner();
|
|
747
848
|
if (!peSigner) return;
|
|
748
849
|
(async () => {
|
|
@@ -755,46 +856,14 @@ function publishEncrypted(context, windowId, id, recipient, encryption, eventTem
|
|
|
755
856
|
replyPe(false, { error: "signEvent returned null" });
|
|
756
857
|
return;
|
|
757
858
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
} catch {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
859
|
+
publishSignedRelayEvent(context, windowId, id, signed, (ok, error) => {
|
|
860
|
+
replyPe(ok, ok ? { event: signed, eventId: signed.id } : { error: error ?? "publish failed" });
|
|
861
|
+
});
|
|
764
862
|
} catch (err) {
|
|
765
863
|
replyPe(false, { error: err?.message ?? "encryption failed" });
|
|
766
864
|
}
|
|
767
865
|
})();
|
|
768
866
|
}
|
|
769
|
-
function publishSignedEncrypted(context, windowId, id, signed, replyPe) {
|
|
770
|
-
const { hooks } = context;
|
|
771
|
-
const relayService = relayServiceFrom(context);
|
|
772
|
-
if (!relayService) {
|
|
773
|
-
if (hooks.relayPool?.isAvailable()) {
|
|
774
|
-
hooks.relayPool.publish(signed);
|
|
775
|
-
replyPe(true, { event: signed, eventId: signed.id });
|
|
776
|
-
} else {
|
|
777
|
-
replyPe(false, { error: "no relay pool available" });
|
|
778
|
-
}
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
const publishMsg = { type: "relay.publish", id, event: signed };
|
|
782
|
-
let replied = false;
|
|
783
|
-
relayService.handleMessage(windowId, publishMsg, (resp) => {
|
|
784
|
-
if (replied) return;
|
|
785
|
-
const r = resp;
|
|
786
|
-
if (typeof r.type !== "string" || !r.type.startsWith("relay.publish")) return;
|
|
787
|
-
const okVal = r.ok ?? r.accepted ?? false;
|
|
788
|
-
replied = true;
|
|
789
|
-
const publishResult = { event: signed, eventId: signed.id };
|
|
790
|
-
if (!okVal) publishResult.error = r.error ?? r.message ?? "publish failed";
|
|
791
|
-
replyPe(okVal, publishResult);
|
|
792
|
-
});
|
|
793
|
-
if (!replied) {
|
|
794
|
-
replied = true;
|
|
795
|
-
replyPe(true, { event: signed, eventId: signed.id });
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
867
|
function handleRelayQuery(context, windowId, m) {
|
|
799
868
|
const { hooks, serviceRegistry, eventBuffer } = context;
|
|
800
869
|
const id = m.id ?? "";
|
|
@@ -921,6 +990,63 @@ function handleRelayQuery(context, windowId, m) {
|
|
|
921
990
|
}, relayUrls);
|
|
922
991
|
}
|
|
923
992
|
|
|
993
|
+
// src/domain-results.ts
|
|
994
|
+
var RUNTIME_THEME_FALLBACK = {
|
|
995
|
+
colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
|
|
996
|
+
};
|
|
997
|
+
var IDENTITY_SAFE_DEFAULTS = {
|
|
998
|
+
"identity.getPublicKey": { pubkey: "" },
|
|
999
|
+
"identity.getRelays": { relays: {} },
|
|
1000
|
+
"identity.getProfile": { profile: null },
|
|
1001
|
+
"identity.getFollows": { pubkeys: [] },
|
|
1002
|
+
"identity.getList": { entries: [] },
|
|
1003
|
+
"identity.getZaps": { zaps: [] },
|
|
1004
|
+
"identity.getMutes": { pubkeys: [] },
|
|
1005
|
+
"identity.getBlocked": { pubkeys: [] },
|
|
1006
|
+
"identity.getBadges": { badges: [] }
|
|
1007
|
+
};
|
|
1008
|
+
function isRuntimeOwnedIdentityOrThemeRequest(type) {
|
|
1009
|
+
return type === "theme.get" || type in IDENTITY_SAFE_DEFAULTS;
|
|
1010
|
+
}
|
|
1011
|
+
function createRuntimeOwnedResultEnvelope(type, id) {
|
|
1012
|
+
return Object.assign({ type }, { id });
|
|
1013
|
+
}
|
|
1014
|
+
function isIdentityOrThemeMessage(message) {
|
|
1015
|
+
return message.type.startsWith("identity.") || message.type.startsWith("theme.");
|
|
1016
|
+
}
|
|
1017
|
+
function createIntentPolicyDenial(message) {
|
|
1018
|
+
const id = message.id ?? "";
|
|
1019
|
+
if (message.type === "intent.invoke") {
|
|
1020
|
+
return {
|
|
1021
|
+
type: "intent.invoke.result",
|
|
1022
|
+
id,
|
|
1023
|
+
result: { ok: false, error: "invoke rejected" }
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
if (message.type === "intent.available" || message.type === "intent.handlers") {
|
|
1027
|
+
return {
|
|
1028
|
+
type: `${message.type}.result`,
|
|
1029
|
+
id,
|
|
1030
|
+
error: "intent request denied"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return void 0;
|
|
1034
|
+
}
|
|
1035
|
+
function createCanonicalDomainResult(message) {
|
|
1036
|
+
if (!isRuntimeOwnedIdentityOrThemeRequest(message.type)) return void 0;
|
|
1037
|
+
const id = message.id ?? "";
|
|
1038
|
+
if (message.type === "theme.get") {
|
|
1039
|
+
return Object.assign(
|
|
1040
|
+
createRuntimeOwnedResultEnvelope("theme.get.result", id),
|
|
1041
|
+
{ theme: RUNTIME_THEME_FALLBACK }
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
return Object.assign(
|
|
1045
|
+
createRuntimeOwnedResultEnvelope(`${message.type}.result`, id),
|
|
1046
|
+
IDENTITY_SAFE_DEFAULTS[message.type]
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
924
1050
|
// src/identity-handler.ts
|
|
925
1051
|
function createIdentityHandler(context) {
|
|
926
1052
|
return function handleIdentityMessage(windowId, msg) {
|
|
@@ -930,14 +1056,12 @@ function createIdentityHandler(context) {
|
|
|
930
1056
|
identityService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
931
1057
|
return;
|
|
932
1058
|
}
|
|
933
|
-
const id = msg.id ?? "";
|
|
934
1059
|
const action = msg.type.slice("identity.".length);
|
|
935
1060
|
const signer = hooks.auth.getSigner();
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
hooks.sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
1061
|
+
const fallback = createCanonicalDomainResult(msg);
|
|
1062
|
+
if (!fallback) return;
|
|
1063
|
+
const sendResult = (payload = {}) => {
|
|
1064
|
+
hooks.sendToNapplet(windowId, { ...fallback, ...payload });
|
|
941
1065
|
};
|
|
942
1066
|
switch (action) {
|
|
943
1067
|
case "getPublicKey":
|
|
@@ -945,14 +1069,14 @@ function createIdentityHandler(context) {
|
|
|
945
1069
|
sendResult({ pubkey: "" });
|
|
946
1070
|
return;
|
|
947
1071
|
}
|
|
948
|
-
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey: pubkey ?? "" })).catch((
|
|
1072
|
+
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey: pubkey ?? "" })).catch(() => sendResult({ pubkey: "" }));
|
|
949
1073
|
return;
|
|
950
1074
|
case "getRelays":
|
|
951
1075
|
if (!signer) {
|
|
952
|
-
|
|
1076
|
+
sendResult();
|
|
953
1077
|
return;
|
|
954
1078
|
}
|
|
955
|
-
Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch((
|
|
1079
|
+
Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch(() => sendResult());
|
|
956
1080
|
return;
|
|
957
1081
|
case "getProfile":
|
|
958
1082
|
sendResult({ profile: null });
|
|
@@ -976,7 +1100,7 @@ function createIdentityHandler(context) {
|
|
|
976
1100
|
sendResult({ badges: [] });
|
|
977
1101
|
return;
|
|
978
1102
|
default:
|
|
979
|
-
|
|
1103
|
+
return;
|
|
980
1104
|
}
|
|
981
1105
|
};
|
|
982
1106
|
}
|
|
@@ -1043,7 +1167,8 @@ function isPlainObject(value) {
|
|
|
1043
1167
|
}
|
|
1044
1168
|
|
|
1045
1169
|
// src/inc-handler.ts
|
|
1046
|
-
|
|
1170
|
+
var MAX_CHANNELS_PER_WINDOW = 32;
|
|
1171
|
+
function createIncRuntime(hooks, sessionRegistry, authorizeChannelOpen) {
|
|
1047
1172
|
const state = {
|
|
1048
1173
|
subscriptions: /* @__PURE__ */ new Map(),
|
|
1049
1174
|
channels: /* @__PURE__ */ new Map(),
|
|
@@ -1051,12 +1176,15 @@ function createIncRuntime(hooks, sessionRegistry) {
|
|
|
1051
1176
|
};
|
|
1052
1177
|
return {
|
|
1053
1178
|
handleMessage(windowId, msg) {
|
|
1054
|
-
handleIncMessage(state, hooks, sessionRegistry, windowId, msg);
|
|
1179
|
+
handleIncMessage(state, hooks, sessionRegistry, authorizeChannelOpen, windowId, msg);
|
|
1055
1180
|
},
|
|
1056
1181
|
destroyWindow(windowId) {
|
|
1057
1182
|
removeWindowChannels(state, hooks, windowId);
|
|
1058
1183
|
removeWindowSubscriptions(state, windowId);
|
|
1059
1184
|
},
|
|
1185
|
+
revokeWindow(windowId) {
|
|
1186
|
+
revokeWindowChannels(state, hooks, windowId);
|
|
1187
|
+
},
|
|
1060
1188
|
clear() {
|
|
1061
1189
|
state.subscriptions.clear();
|
|
1062
1190
|
state.channels.clear();
|
|
@@ -1094,18 +1222,18 @@ function peerOf(state, channelId, self) {
|
|
|
1094
1222
|
return null;
|
|
1095
1223
|
}
|
|
1096
1224
|
function resolveTarget(sessionRegistry, target) {
|
|
1097
|
-
|
|
1098
|
-
const entries = sessionRegistry.getAllEntries();
|
|
1099
|
-
const byPubkey = entries.find((entry) => entry.pubkey === target);
|
|
1100
|
-
return byPubkey?.windowId ?? null;
|
|
1225
|
+
return sessionRegistry.getWindowIdByDTag(target) ?? null;
|
|
1101
1226
|
}
|
|
1102
|
-
function
|
|
1227
|
+
function hasReachedChannelLimit(state, windowId) {
|
|
1228
|
+
return (state.channelsByWindow.get(windowId)?.size ?? 0) >= MAX_CHANNELS_PER_WINDOW;
|
|
1229
|
+
}
|
|
1230
|
+
function handleIncMessage(state, hooks, sessionRegistry, authorizeChannelOpen, windowId, msg) {
|
|
1103
1231
|
const m = msg;
|
|
1104
1232
|
const dotIdx = msg.type.indexOf(".");
|
|
1105
1233
|
const action = msg.type.slice(dotIdx + 1);
|
|
1106
1234
|
switch (action) {
|
|
1107
1235
|
case "emit":
|
|
1108
|
-
handleEmit(state, hooks, windowId, m);
|
|
1236
|
+
handleEmit(state, hooks, sessionRegistry, windowId, m);
|
|
1109
1237
|
return;
|
|
1110
1238
|
case "subscribe":
|
|
1111
1239
|
handleSubscribe(state, hooks, windowId, m);
|
|
@@ -1114,16 +1242,16 @@ function handleIncMessage(state, hooks, sessionRegistry, windowId, msg) {
|
|
|
1114
1242
|
handleUnsubscribe(state, windowId, m);
|
|
1115
1243
|
return;
|
|
1116
1244
|
case "channel.open":
|
|
1117
|
-
handleChannelOpen(state, hooks, sessionRegistry, windowId, m);
|
|
1245
|
+
handleChannelOpen(state, hooks, sessionRegistry, authorizeChannelOpen, windowId, m);
|
|
1118
1246
|
return;
|
|
1119
1247
|
case "channel.emit":
|
|
1120
|
-
handleChannelEmit(state, hooks, windowId, m);
|
|
1248
|
+
handleChannelEmit(state, hooks, sessionRegistry, windowId, m);
|
|
1121
1249
|
return;
|
|
1122
1250
|
case "channel.broadcast":
|
|
1123
|
-
handleChannelBroadcast(state, hooks, windowId, m);
|
|
1251
|
+
handleChannelBroadcast(state, hooks, sessionRegistry, windowId, m);
|
|
1124
1252
|
return;
|
|
1125
1253
|
case "channel.list":
|
|
1126
|
-
handleChannelList(state, hooks, windowId, m);
|
|
1254
|
+
handleChannelList(state, hooks, sessionRegistry, windowId, m);
|
|
1127
1255
|
return;
|
|
1128
1256
|
case "channel.close":
|
|
1129
1257
|
handleChannelClose(state, hooks, windowId, m);
|
|
@@ -1132,14 +1260,16 @@ function handleIncMessage(state, hooks, sessionRegistry, windowId, msg) {
|
|
|
1132
1260
|
return;
|
|
1133
1261
|
}
|
|
1134
1262
|
}
|
|
1135
|
-
function handleEmit(state, hooks, windowId, m) {
|
|
1263
|
+
function handleEmit(state, hooks, sessionRegistry, windowId, m) {
|
|
1136
1264
|
const topic = m.topic ?? "";
|
|
1265
|
+
const sender = sessionRegistry.getEntryByWindowId(windowId)?.dTag;
|
|
1266
|
+
if (!sender) return;
|
|
1137
1267
|
if (!topic) return;
|
|
1138
1268
|
const subscribers = state.subscriptions.get(topic);
|
|
1139
1269
|
if (!subscribers) return;
|
|
1140
1270
|
for (const subscriberWindowId of subscribers) {
|
|
1141
1271
|
if (subscriberWindowId !== windowId) {
|
|
1142
|
-
hooks.sendToNapplet(subscriberWindowId, { type: "inc.event", topic, payload: m.payload, sender
|
|
1272
|
+
hooks.sendToNapplet(subscriberWindowId, { type: "inc.event", topic, payload: m.payload, sender });
|
|
1143
1273
|
}
|
|
1144
1274
|
}
|
|
1145
1275
|
}
|
|
@@ -1166,51 +1296,96 @@ function handleUnsubscribe(state, windowId, m) {
|
|
|
1166
1296
|
subscriptions.delete(windowId);
|
|
1167
1297
|
if (subscriptions.size === 0) state.subscriptions.delete(topic);
|
|
1168
1298
|
}
|
|
1169
|
-
function handleChannelOpen(state, hooks, sessionRegistry, windowId, m) {
|
|
1299
|
+
function handleChannelOpen(state, hooks, sessionRegistry, authorizeChannelOpen, windowId, m) {
|
|
1170
1300
|
const id = m.id ?? "";
|
|
1171
1301
|
const peerWindow = resolveTarget(sessionRegistry, m.target ?? "");
|
|
1172
|
-
|
|
1302
|
+
const openerDTag = sessionRegistry.getEntryByWindowId(windowId)?.dTag;
|
|
1303
|
+
const peerDTag = peerWindow ? sessionRegistry.getEntryByWindowId(peerWindow)?.dTag : void 0;
|
|
1304
|
+
if (!peerWindow || !openerDTag || !peerDTag) {
|
|
1173
1305
|
hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "target not found" });
|
|
1174
1306
|
return;
|
|
1175
1307
|
}
|
|
1308
|
+
if (!authorizeChannelOpen(peerWindow, m)) {
|
|
1309
|
+
hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "target denied" });
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (hasReachedChannelLimit(state, windowId) || hasReachedChannelLimit(state, peerWindow)) {
|
|
1313
|
+
hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "channel limit reached" });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1176
1316
|
const channelId = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
|
|
1177
1317
|
addChannel(state, channelId, windowId, peerWindow);
|
|
1178
|
-
|
|
1318
|
+
try {
|
|
1319
|
+
hooks.sendToNapplet(peerWindow, { type: "inc.channel.opened", channelId, peer: openerDTag });
|
|
1320
|
+
} catch {
|
|
1321
|
+
removeChannel(state, channelId);
|
|
1322
|
+
hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "target unavailable" });
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, channelId, peer: peerDTag });
|
|
1327
|
+
} catch {
|
|
1328
|
+
teardownChannel(state, hooks, channelId);
|
|
1329
|
+
}
|
|
1179
1330
|
}
|
|
1180
|
-
function handleChannelEmit(state, hooks, windowId, m) {
|
|
1331
|
+
function handleChannelEmit(state, hooks, sessionRegistry, windowId, m) {
|
|
1181
1332
|
const peer = peerOf(state, m.channelId ?? "", windowId);
|
|
1182
|
-
|
|
1183
|
-
|
|
1333
|
+
const sender = sessionRegistry.getEntryByWindowId(windowId)?.dTag;
|
|
1334
|
+
if (peer && sender) {
|
|
1335
|
+
hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId: m.channelId ?? "", sender, payload: m.payload });
|
|
1184
1336
|
}
|
|
1185
1337
|
}
|
|
1186
|
-
function handleChannelBroadcast(state, hooks, windowId, m) {
|
|
1338
|
+
function handleChannelBroadcast(state, hooks, sessionRegistry, windowId, m) {
|
|
1187
1339
|
const channels = state.channelsByWindow.get(windowId);
|
|
1188
|
-
|
|
1340
|
+
const sender = sessionRegistry.getEntryByWindowId(windowId)?.dTag;
|
|
1341
|
+
if (!channels || !sender) return;
|
|
1189
1342
|
for (const channelId of channels) {
|
|
1190
1343
|
const peer = peerOf(state, channelId, windowId);
|
|
1191
1344
|
if (peer) {
|
|
1192
|
-
hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId, sender
|
|
1345
|
+
hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId, sender, payload: m.payload });
|
|
1193
1346
|
}
|
|
1194
1347
|
}
|
|
1195
1348
|
}
|
|
1196
|
-
function handleChannelList(state, hooks, windowId, m) {
|
|
1349
|
+
function handleChannelList(state, hooks, sessionRegistry, windowId, m) {
|
|
1197
1350
|
const channels = [];
|
|
1198
1351
|
const set = state.channelsByWindow.get(windowId);
|
|
1199
1352
|
if (set) {
|
|
1200
1353
|
for (const channelId of set) {
|
|
1201
1354
|
const peer = peerOf(state, channelId, windowId);
|
|
1202
|
-
|
|
1355
|
+
const peerDTag = peer ? sessionRegistry.getEntryByWindowId(peer)?.dTag : void 0;
|
|
1356
|
+
if (peerDTag) channels.push({ id: channelId, peer: peerDTag });
|
|
1203
1357
|
}
|
|
1204
1358
|
}
|
|
1205
1359
|
hooks.sendToNapplet(windowId, { type: "inc.channel.list.result", id: m.id ?? "", channels });
|
|
1206
1360
|
}
|
|
1207
1361
|
function handleChannelClose(state, hooks, windowId, m) {
|
|
1208
1362
|
const channelId = m.channelId ?? "";
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1363
|
+
if (!peerOf(state, channelId, windowId)) return;
|
|
1364
|
+
teardownChannel(state, hooks, channelId);
|
|
1365
|
+
}
|
|
1366
|
+
function teardownChannel(state, hooks, channelId, destroyedWindowId) {
|
|
1367
|
+
const channel = state.channels.get(channelId);
|
|
1368
|
+
if (!channel) return;
|
|
1369
|
+
try {
|
|
1370
|
+
if (destroyedWindowId) {
|
|
1371
|
+
const survivor = peerOf(state, channelId, destroyedWindowId);
|
|
1372
|
+
if (survivor) {
|
|
1373
|
+
try {
|
|
1374
|
+
hooks.sendToNapplet(survivor, { type: "inc.channel.closed", channelId, reason: "peer destroyed" });
|
|
1375
|
+
} catch {
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
for (const peer of [channel.peerA, channel.peerB]) {
|
|
1381
|
+
try {
|
|
1382
|
+
hooks.sendToNapplet(peer, { type: "inc.channel.closed", channelId });
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
} finally {
|
|
1387
|
+
removeChannel(state, channelId);
|
|
1388
|
+
}
|
|
1214
1389
|
}
|
|
1215
1390
|
function removeWindowSubscriptions(state, windowId) {
|
|
1216
1391
|
for (const [topic, subscriptions] of state.subscriptions) {
|
|
@@ -1222,11 +1397,14 @@ function removeWindowChannels(state, hooks, windowId) {
|
|
|
1222
1397
|
const channelIds = state.channelsByWindow.get(windowId);
|
|
1223
1398
|
if (!channelIds) return;
|
|
1224
1399
|
for (const channelId of Array.from(channelIds)) {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1400
|
+
teardownChannel(state, hooks, channelId, windowId);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function revokeWindowChannels(state, hooks, windowId) {
|
|
1404
|
+
const channelIds = state.channelsByWindow.get(windowId);
|
|
1405
|
+
if (!channelIds) return;
|
|
1406
|
+
for (const channelId of Array.from(channelIds)) {
|
|
1407
|
+
teardownChannel(state, hooks, channelId);
|
|
1230
1408
|
}
|
|
1231
1409
|
}
|
|
1232
1410
|
|
|
@@ -1377,9 +1555,6 @@ function cleanupNappState(pubkey, dTag, aggregateHash, statePersistence) {
|
|
|
1377
1555
|
}
|
|
1378
1556
|
|
|
1379
1557
|
// src/domain-handlers.ts
|
|
1380
|
-
var THEME_FALLBACK_DEFAULT = {
|
|
1381
|
-
colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
|
|
1382
|
-
};
|
|
1383
1558
|
function createRuntimeDomainHandlers(context) {
|
|
1384
1559
|
return {
|
|
1385
1560
|
storage: (windowId, msg) => handleStorageMessage(context, windowId, msg),
|
|
@@ -1441,8 +1616,9 @@ function handleMediaMessage(context, windowId, msg) {
|
|
|
1441
1616
|
}
|
|
1442
1617
|
}
|
|
1443
1618
|
function handleKeysMessage(context, windowId, msg) {
|
|
1444
|
-
const { hooks, serviceRegistry } = context;
|
|
1619
|
+
const { hooks, serviceRegistry, sessionRegistry } = context;
|
|
1445
1620
|
if (msg.type === "keys.forward") {
|
|
1621
|
+
if (!sessionRegistry.getEntryByWindowId(windowId)) return;
|
|
1446
1622
|
forwardHotkey(hooks, msg);
|
|
1447
1623
|
return;
|
|
1448
1624
|
}
|
|
@@ -1469,8 +1645,7 @@ function sendRegisterActionResult(hooks, windowId, msg) {
|
|
|
1469
1645
|
hooks.sendToNapplet(windowId, {
|
|
1470
1646
|
type: "keys.registerAction.result",
|
|
1471
1647
|
id: m.id ?? "",
|
|
1472
|
-
actionId: m.action?.id ?? ""
|
|
1473
|
-
...m.action?.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
1648
|
+
actionId: m.action?.id ?? ""
|
|
1474
1649
|
});
|
|
1475
1650
|
}
|
|
1476
1651
|
function handleNotifyMessage(context, windowId, msg) {
|
|
@@ -1495,16 +1670,13 @@ function handleThemeMessage(context, windowId, msg) {
|
|
|
1495
1670
|
themeService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1496
1671
|
return;
|
|
1497
1672
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
hooks.sendToNapplet(windowId, {
|
|
1501
|
-
type: "theme.get.result",
|
|
1502
|
-
id: m.id ?? "",
|
|
1503
|
-
theme: THEME_FALLBACK_DEFAULT
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1673
|
+
const fallback = createCanonicalDomainResult(msg);
|
|
1674
|
+
if (fallback) hooks.sendToNapplet(windowId, fallback);
|
|
1506
1675
|
}
|
|
1507
1676
|
function handleServiceOnlyMessage(context, name, windowId, msg) {
|
|
1677
|
+
if (name === "intent" && msg.type !== "intent.invoke" && msg.type !== "intent.available" && msg.type !== "intent.handlers") {
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1508
1680
|
const service = context.serviceRegistry[name];
|
|
1509
1681
|
if (!service) return;
|
|
1510
1682
|
service.handleMessage(windowId, msg, (resp) => context.hooks.sendToNapplet(windowId, resp));
|
|
@@ -1592,7 +1764,20 @@ function buildObservation(envelope, windowId, senderCap, sessionRegistry, getFoc
|
|
|
1592
1764
|
const focus = getFocusContext?.(windowId) ?? { focused: true };
|
|
1593
1765
|
const opClass = senderCap ?? envelope.type;
|
|
1594
1766
|
const { kind, size } = extractKindSize(envelope);
|
|
1595
|
-
return {
|
|
1767
|
+
return {
|
|
1768
|
+
napplet,
|
|
1769
|
+
opClass,
|
|
1770
|
+
kind,
|
|
1771
|
+
size,
|
|
1772
|
+
initElapsedMs,
|
|
1773
|
+
// `windowId` comes from the host's source-bound session lookup, never the
|
|
1774
|
+
// napplet message envelope. It scopes startup burst accounting to this
|
|
1775
|
+
// registered realm while rate limits remain dTag-wide.
|
|
1776
|
+
initKey: windowId,
|
|
1777
|
+
focused: focus.focused,
|
|
1778
|
+
msSinceFocusGain: focus.msSinceFocusGain,
|
|
1779
|
+
now
|
|
1780
|
+
};
|
|
1596
1781
|
}
|
|
1597
1782
|
function createFirewallGate(config) {
|
|
1598
1783
|
const { firewallState, sessionRegistry, hooks, fireConsent } = config;
|
|
@@ -1603,10 +1788,25 @@ function createFirewallGate(config) {
|
|
|
1603
1788
|
const napplet = obs.napplet;
|
|
1604
1789
|
const opClass = obs.opClass;
|
|
1605
1790
|
if (decision === "reject" || decision === "prompt") {
|
|
1791
|
+
const intentDenial = createIntentPolicyDenial(envelope);
|
|
1792
|
+
if (intentDenial) {
|
|
1793
|
+
hooks.sendToNapplet(windowId, intentDenial);
|
|
1794
|
+
hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
|
|
1795
|
+
if (decision === "prompt") fireConsent(windowId, napplet);
|
|
1796
|
+
return "drop";
|
|
1797
|
+
}
|
|
1798
|
+
const canonicalResult = createCanonicalDomainResult(envelope);
|
|
1799
|
+
if (canonicalResult) {
|
|
1800
|
+
hooks.sendToNapplet(windowId, canonicalResult);
|
|
1801
|
+
hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
|
|
1802
|
+
if (decision === "prompt") fireConsent(windowId, napplet);
|
|
1803
|
+
return "drop";
|
|
1804
|
+
}
|
|
1606
1805
|
const id = envelope.id ?? "";
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1806
|
+
const type = denialResponseType(envelope);
|
|
1807
|
+
if (type) {
|
|
1808
|
+
hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
|
|
1809
|
+
}
|
|
1610
1810
|
hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
|
|
1611
1811
|
if (decision === "prompt") {
|
|
1612
1812
|
fireConsent(windowId, napplet);
|
|
@@ -1619,21 +1819,45 @@ function createFirewallGate(config) {
|
|
|
1619
1819
|
return "dispatch";
|
|
1620
1820
|
};
|
|
1621
1821
|
}
|
|
1622
|
-
function
|
|
1822
|
+
function denialResponseType(envelope) {
|
|
1823
|
+
if (envelope.type.startsWith("intent.")) return null;
|
|
1824
|
+
if (envelope.type.startsWith("storage.")) return `${envelope.type}.result`;
|
|
1825
|
+
if (envelope.type === "inc.subscribe" || envelope.type === "inc.channel.open") {
|
|
1826
|
+
return `${envelope.type}.result`;
|
|
1827
|
+
}
|
|
1828
|
+
if (envelope.type.startsWith("inc.")) return null;
|
|
1829
|
+
return `${envelope.type}.error`;
|
|
1830
|
+
}
|
|
1831
|
+
function createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnvelope, firewallGate) {
|
|
1623
1832
|
return (windowId, msg) => {
|
|
1624
1833
|
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
|
|
1625
1834
|
const envelope = msg;
|
|
1626
1835
|
const dotIdx = envelope.type.indexOf(".");
|
|
1627
1836
|
if (dotIdx === -1) return;
|
|
1837
|
+
if (!sessionRegistry.getEntryByWindowId(windowId)) return;
|
|
1838
|
+
const domain = envelope.type.slice(0, dotIdx);
|
|
1839
|
+
if (hooks.isDomainAllowed && !hooks.isDomainAllowed(windowId, domain)) return;
|
|
1840
|
+
if (isIdentityOrThemeMessage(envelope) && !createCanonicalDomainResult(envelope)) return;
|
|
1628
1841
|
const caps = resolveCapabilitiesNap(envelope);
|
|
1629
1842
|
if (caps.senderCap) {
|
|
1630
1843
|
const result = enforceNap(windowId, caps.senderCap, envelope);
|
|
1631
1844
|
if (!result.allowed) {
|
|
1845
|
+
const intentDenial = createIntentPolicyDenial(envelope);
|
|
1846
|
+
if (intentDenial) {
|
|
1847
|
+
hooks.sendToNapplet(windowId, intentDenial);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const canonicalResult = createCanonicalDomainResult(envelope);
|
|
1851
|
+
if (canonicalResult) {
|
|
1852
|
+
hooks.sendToNapplet(windowId, canonicalResult);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1632
1855
|
const id = envelope.id ?? "";
|
|
1633
|
-
const isStorageEnvelope = envelope.type.startsWith("storage.");
|
|
1634
1856
|
const error = formatDenialReason(result.capability);
|
|
1635
|
-
const type =
|
|
1636
|
-
|
|
1857
|
+
const type = denialResponseType(envelope);
|
|
1858
|
+
if (type) {
|
|
1859
|
+
hooks.sendToNapplet(windowId, { type, id, error });
|
|
1860
|
+
}
|
|
1637
1861
|
return;
|
|
1638
1862
|
}
|
|
1639
1863
|
}
|
|
@@ -1670,12 +1894,64 @@ function createRuntimeInstance(context) {
|
|
|
1670
1894
|
consentHandlerRef
|
|
1671
1895
|
} = context;
|
|
1672
1896
|
const undeclaredServiceConsents = /* @__PURE__ */ new Set();
|
|
1897
|
+
const attachedServices = /* @__PURE__ */ new Map();
|
|
1898
|
+
const isDomainAllowed = hooks.isDomainAllowed;
|
|
1899
|
+
const sendToNapplet = hooks.sendToNapplet;
|
|
1900
|
+
const serviceContext = Object.freeze({
|
|
1901
|
+
resolveDTag(windowId) {
|
|
1902
|
+
return sessionRegistry.getEntryByWindowId(windowId)?.dTag;
|
|
1903
|
+
},
|
|
1904
|
+
listWindowIds() {
|
|
1905
|
+
return Object.freeze(
|
|
1906
|
+
sessionRegistry.getAllEntries().map((entry) => entry.windowId)
|
|
1907
|
+
);
|
|
1908
|
+
},
|
|
1909
|
+
sendToEligibleNapplet(windowId, message) {
|
|
1910
|
+
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
1911
|
+
if (!entry || typeof message.type !== "string") return false;
|
|
1912
|
+
const dotIndex = message.type.indexOf(".");
|
|
1913
|
+
if (dotIndex <= 0) return false;
|
|
1914
|
+
const domain = message.type.slice(0, dotIndex);
|
|
1915
|
+
try {
|
|
1916
|
+
if (isDomainAllowed && !isDomainAllowed(windowId, domain)) return false;
|
|
1917
|
+
const { recipientCap } = resolveCapabilitiesNap(message);
|
|
1918
|
+
if (!recipientCap) return false;
|
|
1919
|
+
if (!aclState.check("", entry.dTag, entry.aggregateHash, recipientCap)) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
sendToNapplet(windowId, message);
|
|
1923
|
+
return true;
|
|
1924
|
+
} catch {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
function attachService(name, handler) {
|
|
1930
|
+
attachedServices.set(name, handler);
|
|
1931
|
+
try {
|
|
1932
|
+
handler.onRegistered?.(serviceContext);
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function detachService(name) {
|
|
1937
|
+
const handler = attachedServices.get(name);
|
|
1938
|
+
if (!handler) return;
|
|
1939
|
+
attachedServices.delete(name);
|
|
1940
|
+
try {
|
|
1941
|
+
handler.onUnregistered?.();
|
|
1942
|
+
} catch {
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
for (const [name, handler] of Object.entries(serviceRegistry)) {
|
|
1946
|
+
attachService(name, handler);
|
|
1947
|
+
}
|
|
1673
1948
|
return {
|
|
1674
1949
|
handleMessage: context.handleMessage,
|
|
1675
1950
|
injectEvent(topic, payload) {
|
|
1676
1951
|
eventBuffer.bufferAndDeliver(createInjectedEvent(hooks, topic, payload), null);
|
|
1677
1952
|
},
|
|
1678
1953
|
destroy() {
|
|
1954
|
+
for (const name of attachedServices.keys()) detachService(name);
|
|
1679
1955
|
manifestCache.persist();
|
|
1680
1956
|
aclState.persist();
|
|
1681
1957
|
firewallState.persist();
|
|
@@ -1690,14 +1966,17 @@ function createRuntimeInstance(context) {
|
|
|
1690
1966
|
consentHandlerRef.current = handler;
|
|
1691
1967
|
},
|
|
1692
1968
|
registerService(name, handler) {
|
|
1969
|
+
detachService(name);
|
|
1693
1970
|
serviceRegistry[name] = handler;
|
|
1694
1971
|
registeredServices.set(name, {
|
|
1695
1972
|
name: handler.descriptor.name,
|
|
1696
1973
|
version: handler.descriptor.version,
|
|
1697
1974
|
description: handler.descriptor.description
|
|
1698
1975
|
});
|
|
1976
|
+
attachService(name, handler);
|
|
1699
1977
|
},
|
|
1700
1978
|
unregisterService(name) {
|
|
1979
|
+
detachService(name);
|
|
1701
1980
|
delete serviceRegistry[name];
|
|
1702
1981
|
registeredServices.delete(name);
|
|
1703
1982
|
},
|
|
@@ -1730,7 +2009,15 @@ function createRuntime(hooks) {
|
|
|
1730
2009
|
const serviceRegistry = { ...hooks.services };
|
|
1731
2010
|
const registeredServices = createRegisteredServices(serviceRegistry);
|
|
1732
2011
|
const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate);
|
|
1733
|
-
|
|
2012
|
+
let incRuntime = null;
|
|
2013
|
+
const aclState = createAclState(hooks.aclPersistence, "permissive", (mutation) => {
|
|
2014
|
+
if (mutation.type === "revoke" && mutation.capability !== "relay:read") return;
|
|
2015
|
+
for (const entry of sessionRegistry.getAllEntries()) {
|
|
2016
|
+
if (entry.dTag === mutation.dTag && entry.aggregateHash === mutation.aggregateHash) {
|
|
2017
|
+
incRuntime?.revokeWindow(entry.windowId);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
1734
2021
|
const firewallState = createFirewallState(hooks.firewallPersistence);
|
|
1735
2022
|
const manifestCache = createManifestCache(hooks.manifestPersistence);
|
|
1736
2023
|
const replayDetector = createReplayDetector(
|
|
@@ -1767,6 +2054,11 @@ function createRuntime(hooks) {
|
|
|
1767
2054
|
},
|
|
1768
2055
|
onAclCheck: hooks.onAclCheck
|
|
1769
2056
|
});
|
|
2057
|
+
incRuntime = createIncRuntime(
|
|
2058
|
+
hooks,
|
|
2059
|
+
sessionRegistry,
|
|
2060
|
+
(targetWindowId, message) => enforceNap(targetWindowId, "relay:read", message).allowed
|
|
2061
|
+
);
|
|
1770
2062
|
const firewallGate = createFirewallGate({ firewallState, sessionRegistry, hooks, fireConsent });
|
|
1771
2063
|
const eventBuffer = createEventBuffer(
|
|
1772
2064
|
hooks.sendToNapplet,
|
|
@@ -1778,7 +2070,6 @@ function createRuntime(hooks) {
|
|
|
1778
2070
|
aclState.load();
|
|
1779
2071
|
firewallState.load();
|
|
1780
2072
|
manifestCache.load();
|
|
1781
|
-
const incRuntime = createIncRuntime(hooks, sessionRegistry);
|
|
1782
2073
|
const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry, aclState });
|
|
1783
2074
|
const dispatchNapEnvelope = createNapEnvelopeDispatcher({
|
|
1784
2075
|
relay: createRelayHandler({ hooks, serviceRegistry, subscriptions, eventBuffer, replayDetector }),
|
|
@@ -1787,7 +2078,7 @@ function createRuntime(hooks) {
|
|
|
1787
2078
|
inc: incRuntime.handleMessage,
|
|
1788
2079
|
...domainHandlers
|
|
1789
2080
|
});
|
|
1790
|
-
const handleMessage = createMessageHandler(hooks, enforceNap, dispatchNapEnvelope, firewallGate);
|
|
2081
|
+
const handleMessage = createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnvelope, firewallGate);
|
|
1791
2082
|
return createRuntimeInstance({
|
|
1792
2083
|
hooks,
|
|
1793
2084
|
serviceRegistry,
|