@kehto/runtime 0.24.1 → 0.25.1
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 +29 -1
- package/dist/index.d.ts +40 -16
- package/dist/index.js +133 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,20 @@ The runtime is built around the current draft dispatch contract from `@napplet/c
|
|
|
40
40
|
|
|
41
41
|
Signing is shell-mediated inside `relay.publish` / `relay.publishEncrypted` (NIP-44 default, NIP-04 opt-in). The legacy signer domain is dissolved — napplets never see a host-injected nostr object and cannot call signer-sign RPCs directly.
|
|
42
42
|
|
|
43
|
+
### NAP-RELAY subscription revocation
|
|
44
|
+
|
|
45
|
+
Revoking `relay:read` or blocking a principal immediately retires ordinary relay
|
|
46
|
+
subscriptions in every window with that exact `dTag` and aggregate hash. The
|
|
47
|
+
runtime closes registered relay/cache backends and direct adapter subscriptions,
|
|
48
|
+
sends `relay.closed` with reason `denied: relay:read`, and suppresses queued events
|
|
49
|
+
and EOSE callbacks. Regranting access requires a new subscription; old callbacks
|
|
50
|
+
remain invalid even if the same `subId` is reused. Other principals, other artifact
|
|
51
|
+
versions, and revocation of unrelated capabilities leave subscriptions active.
|
|
52
|
+
|
|
53
|
+
This host policy conforms to [NAP-RELAY draft PR #2 at
|
|
54
|
+
`9f30b3505335a52c574ff570576687d6467021ff`](https://github.com/napplet/naps/blob/9f30b3505335a52c574ff570576687d6467021ff/naps/NAP-RELAY.md),
|
|
55
|
+
which permits ACL enforcement and policy-driven subscription termination.
|
|
56
|
+
|
|
43
57
|
### NAP-RELAY publish boundary
|
|
44
58
|
|
|
45
59
|
The publish path follows draft [NAP-RELAY PR #2 at
|
|
@@ -223,10 +237,24 @@ implementations of `FirewallStateContainer` must provide this method.
|
|
|
223
237
|
- `cleanupNappState` — remove persisted state when a napplet window closes
|
|
224
238
|
|
|
225
239
|
### Service dispatch
|
|
226
|
-
- `routeServiceMessage` —
|
|
240
|
+
- `routeServiceMessage` — policy-free domain selection primitive; never a fallback for untrusted ingress
|
|
227
241
|
- `notifyServiceWindowDestroyed` — lifecycle fan-out to every service handler
|
|
228
242
|
- Service-only NAP domains, including `dm` and `fs`, route through handlers registered with `runtime.registerService('<domain>', handler)`.
|
|
229
243
|
|
|
244
|
+
For domains outside the built-in dispatcher, handlers opt in with an exact
|
|
245
|
+
`messageCapabilities` map (existing ACL capability or explicit `null` per inbound
|
|
246
|
+
type). `Runtime.handleMessage` requires a live unblocked source, an explicit
|
|
247
|
+
`isDomainAllowed` grant, the declared ACL grant, and firewall admission. Unknown
|
|
248
|
+
types and extension policy denials stay silent on the wire. Registration alone
|
|
249
|
+
attaches a service; it does not expose or grant a domain. Fixed NAP routes retain
|
|
250
|
+
their own authorization. Pending replies are invalidated by service/source
|
|
251
|
+
replacement or teardown and recheck current domain/ACL policy.
|
|
252
|
+
|
|
253
|
+
Browser hosts must provide the owning extension contract and inject its granted
|
|
254
|
+
namespace binding before scripts run. Stock Paja/playground bindings do not inject
|
|
255
|
+
arbitrary extensions. See [Register a service](../../docs/how-tos/register-service.md#domains-outside-the-built-in-dispatcher)
|
|
256
|
+
for the complete host contract and spec bounds.
|
|
257
|
+
|
|
230
258
|
### Types
|
|
231
259
|
40+ interfaces — including `Runtime`, `RuntimeAdapter`, `SendToNapplet`, `RelayPoolAdapter`, `ServiceHandler`, `ServiceRegistry`, `NappletMessage`, `SessionEntry`, `AclEntryExternal`, `AclCheckEvent`, and the per-adapter hook types — are exported from `./types.js` for host-app integration.
|
|
232
260
|
|
package/dist/index.d.ts
CHANGED
|
@@ -583,12 +583,12 @@ interface ServiceRuntimeContext {
|
|
|
583
583
|
*
|
|
584
584
|
* @example
|
|
585
585
|
* ```ts
|
|
586
|
-
* const
|
|
587
|
-
* descriptor: { name: '
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
* send({ type: '
|
|
586
|
+
* const exampleHandler: ServiceHandler = {
|
|
587
|
+
* descriptor: { name: 'example', version: '1.0.0' },
|
|
588
|
+
* messageCapabilities: { 'example.ping': null },
|
|
589
|
+
* handleMessage(_windowId, message, send) {
|
|
590
|
+
* if (message.type === 'example.ping') {
|
|
591
|
+
* send({ type: 'example.pong' });
|
|
592
592
|
* }
|
|
593
593
|
* },
|
|
594
594
|
* };
|
|
@@ -597,6 +597,19 @@ interface ServiceRuntimeContext {
|
|
|
597
597
|
interface ServiceHandler {
|
|
598
598
|
/** Metadata describing this service. */
|
|
599
599
|
descriptor: ServiceDescriptor;
|
|
600
|
+
/**
|
|
601
|
+
* Explicit inbound types for a domain outside the built-in dispatcher.
|
|
602
|
+
* Each own key is an exact `domain.action` type; its value is an existing
|
|
603
|
+
* ACL capability or explicit `null` for no additional capability requirement.
|
|
604
|
+
* Omission keeps non-built-in services attachment-only. Built-in NAP routes
|
|
605
|
+
* ignore this map and retain their canonical capability mapping.
|
|
606
|
+
*
|
|
607
|
+
* Extension dispatch also requires a live, unblocked source, an explicit
|
|
608
|
+
* `RuntimeAdapter.isDomainAllowed` grant, and firewall admission. This map
|
|
609
|
+
* neither injects a namespace binding nor grants any capability. The handler
|
|
610
|
+
* owns payload validation and its extension's response contract.
|
|
611
|
+
*/
|
|
612
|
+
messageCapabilities?: Readonly<Record<string, Capability | null>>;
|
|
600
613
|
/**
|
|
601
614
|
* Attach this handler to one runtime instance.
|
|
602
615
|
*
|
|
@@ -623,14 +636,14 @@ interface ServiceHandler {
|
|
|
623
636
|
}
|
|
624
637
|
/**
|
|
625
638
|
* Registry of services available to napplets.
|
|
626
|
-
*
|
|
627
|
-
*
|
|
639
|
+
* Keys select exact wire domains for direct routing (e.g., 'notify').
|
|
640
|
+
* Metadata/attachment alone does not expose a domain. Custom domains require
|
|
641
|
+
* ServiceHandler.messageCapabilities and explicit host domain authorization.
|
|
628
642
|
*
|
|
629
643
|
* @example
|
|
630
644
|
* ```ts
|
|
631
645
|
* const services: ServiceRegistry = {
|
|
632
|
-
*
|
|
633
|
-
* notifications: notificationHandler,
|
|
646
|
+
* notify: notificationHandler,
|
|
634
647
|
* };
|
|
635
648
|
* ```
|
|
636
649
|
*/
|
|
@@ -692,8 +705,9 @@ interface RuntimeAdapter {
|
|
|
692
705
|
sendToNapplet: SendToNapplet;
|
|
693
706
|
/**
|
|
694
707
|
* Determines whether a session-established napplet may invoke a NAP domain.
|
|
695
|
-
*
|
|
696
|
-
*
|
|
708
|
+
* Built-in routes remain eligible when omitted; extension routes require
|
|
709
|
+
* an explicit true result. Browser shells must derive this decision from
|
|
710
|
+
* the source-bound namespace exposed before napplet scripts execute.
|
|
697
711
|
*/
|
|
698
712
|
isDomainAllowed?: (windowId: string, domain: string) => boolean;
|
|
699
713
|
/**
|
|
@@ -792,14 +806,17 @@ interface RuntimeAdapter {
|
|
|
792
806
|
/**
|
|
793
807
|
* Optional service extensions. Shell/host registers service handlers here
|
|
794
808
|
* for static initialization. Services can also be added dynamically via
|
|
795
|
-
* runtime.registerService().
|
|
809
|
+
* runtime.registerService(). Keys select exact domains (e.g., 'notify').
|
|
810
|
+
* Outside built-in domains, handlers also need messageCapabilities and an
|
|
811
|
+
* explicit isDomainAllowed grant. Hosts own each extension's NAP contract,
|
|
812
|
+
* payload validation, and pre-execution browser namespace binding.
|
|
796
813
|
*
|
|
797
814
|
* @example
|
|
798
815
|
* ```ts
|
|
799
816
|
* const hooks: RuntimeAdapter = {
|
|
800
817
|
* // ... required adapters ...
|
|
801
818
|
* services: {
|
|
802
|
-
*
|
|
819
|
+
* notify: myNotificationServiceHandler,
|
|
803
820
|
* },
|
|
804
821
|
* };
|
|
805
822
|
* ```
|
|
@@ -1456,7 +1473,11 @@ interface Runtime {
|
|
|
1456
1473
|
* Register a service handler dynamically after runtime creation.
|
|
1457
1474
|
* If a handler is already registered for this name, it is replaced.
|
|
1458
1475
|
*
|
|
1459
|
-
*
|
|
1476
|
+
* Built-in domains keep their NAP routing. Other domains require the
|
|
1477
|
+
* handler's exact messageCapabilities map and an explicit host domain grant.
|
|
1478
|
+
* Registration never injects namespace bindings or grants capabilities.
|
|
1479
|
+
*
|
|
1480
|
+
* @param name - Exact domain name (e.g., 'notify' or a host extension)
|
|
1460
1481
|
* @param handler - The service handler implementation
|
|
1461
1482
|
*/
|
|
1462
1483
|
registerService(name: string, handler: ServiceHandler): void;
|
|
@@ -1580,9 +1601,12 @@ declare function cleanupNappState(pubkey: string, dTag: string, aggregateHash: s
|
|
|
1580
1601
|
* Route a NappletMessage envelope to the matching service handler.
|
|
1581
1602
|
*
|
|
1582
1603
|
* Direct NAP-domain services are routed by the domain in their wire
|
|
1583
|
-
* message.type (e.g., '
|
|
1604
|
+
* message.type (e.g., 'notify.send' -> 'notify' service). INC messages
|
|
1584
1605
|
* are owned by the INC runtime; their topic values are opaque here.
|
|
1585
1606
|
*
|
|
1607
|
+
* This low-level selection primitive performs no source, domain, ACL,
|
|
1608
|
+
* messageCapabilities, or firewall checks. Never call it as a fallback for
|
|
1609
|
+
* untrusted input; host ingress must use Runtime.handleMessage.
|
|
1586
1610
|
* Returns true if a service handled the message, false otherwise.
|
|
1587
1611
|
*
|
|
1588
1612
|
* @param windowId - The napplet window that sent the message
|
package/dist/index.js
CHANGED
|
@@ -565,11 +565,52 @@ function relayEventResultFromCarrier(carrier) {
|
|
|
565
565
|
// src/runtime.ts
|
|
566
566
|
import { createDispatch } from "@napplet/core";
|
|
567
567
|
|
|
568
|
+
// src/service-extensions.ts
|
|
569
|
+
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
570
|
+
function createServiceExtensionRouter(context) {
|
|
571
|
+
const { hooks, services, registrations, sessions, acl, enforce, firewall } = context;
|
|
572
|
+
const windowLifetimes = /* @__PURE__ */ new Map();
|
|
573
|
+
function handleMessage(windowId, message) {
|
|
574
|
+
const dot = message.type.indexOf(".");
|
|
575
|
+
const domain = message.type.slice(0, dot);
|
|
576
|
+
if (dot <= 0 || dot === message.type.length - 1 || !/^[a-z][a-z0-9-]*$/.test(domain) || !Object.hasOwn(services, domain)) return;
|
|
577
|
+
const handler = services[domain];
|
|
578
|
+
const policies = handler.messageCapabilities;
|
|
579
|
+
if (!policies || !Object.hasOwn(policies, message.type)) return;
|
|
580
|
+
const capability = policies[message.type];
|
|
581
|
+
if (capability !== null && !ALL_CAPABILITIES.includes(capability)) return;
|
|
582
|
+
const session = sessions.getEntryByWindowId(windowId);
|
|
583
|
+
const registration = registrations.get(domain);
|
|
584
|
+
if (!session || !registration) return;
|
|
585
|
+
const allowed = () => hooks.isDomainAllowed?.(windowId, domain) === true && !acl.isBlocked("", session.dTag, session.aggregateHash) && (capability === null || enforce(windowId, capability, message).allowed);
|
|
586
|
+
if (!allowed() || firewall(windowId, message, capability, false) === "drop") return;
|
|
587
|
+
let lifetime = windowLifetimes.get(windowId);
|
|
588
|
+
if (!lifetime) {
|
|
589
|
+
lifetime = {};
|
|
590
|
+
windowLifetimes.set(windowId, lifetime);
|
|
591
|
+
}
|
|
592
|
+
handler.handleMessage(windowId, message, (response) => {
|
|
593
|
+
if (registrations.get(domain) !== registration || services[domain] !== handler || windowLifetimes.get(windowId) !== lifetime || sessions.getEntryByWindowId(windowId) !== session) return;
|
|
594
|
+
if (!response || typeof response.type !== "string" || !response.type.startsWith(`${domain}.`) || response.type.length <= dot + 1) return;
|
|
595
|
+
if (allowed()) hooks.sendToNapplet(windowId, response);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
handleMessage,
|
|
600
|
+
destroyWindow(windowId) {
|
|
601
|
+
windowLifetimes.delete(windowId);
|
|
602
|
+
},
|
|
603
|
+
destroy() {
|
|
604
|
+
windowLifetimes.clear();
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
568
609
|
// src/service-dispatch.ts
|
|
569
610
|
function routeServiceMessage(windowId, message, services, sendToNapplet) {
|
|
570
611
|
if (message.type === "inc.emit") return false;
|
|
571
612
|
const domain = message.type.split(".")[0];
|
|
572
|
-
const handler = services[domain];
|
|
613
|
+
const handler = Object.hasOwn(services, domain) ? services[domain] : void 0;
|
|
573
614
|
if (handler) {
|
|
574
615
|
handler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
575
616
|
return true;
|
|
@@ -633,12 +674,17 @@ function handleRelaySubscribe(context, windowId, msg, m) {
|
|
|
633
674
|
const filters = m.filters ?? [];
|
|
634
675
|
if (!subId) return;
|
|
635
676
|
const subKey = `${windowId}:${subId}`;
|
|
636
|
-
|
|
677
|
+
const entry = { windowId, filters };
|
|
678
|
+
subscriptions.set(subKey, entry);
|
|
679
|
+
const isActive = () => subscriptions.get(subKey) === entry && context.canReadRelay(windowId);
|
|
680
|
+
const send = (response) => {
|
|
681
|
+
if (isActive()) hooks.sendToNapplet(windowId, normalizeRelayServiceResponse(response));
|
|
682
|
+
};
|
|
637
683
|
const seenIds = /* @__PURE__ */ new Set();
|
|
638
684
|
function deliver(event, relayHints) {
|
|
639
685
|
if (seenIds.has(event.id)) return;
|
|
640
686
|
seenIds.add(event.id);
|
|
641
|
-
if (
|
|
687
|
+
if (isActive()) {
|
|
642
688
|
sendRelayEvent(hooks, windowId, subId, createRelayEventResultWithHints(event, relayHints));
|
|
643
689
|
}
|
|
644
690
|
}
|
|
@@ -649,22 +695,16 @@ function handleRelaySubscribe(context, windowId, msg, m) {
|
|
|
649
695
|
const relayService = relayServiceFrom(context);
|
|
650
696
|
const cacheService = !serviceRegistry["relay"] ? serviceRegistry["cache"] : void 0;
|
|
651
697
|
if (!isShellKind && relayService) {
|
|
652
|
-
relayService.handleMessage(windowId, msg,
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
});
|
|
656
|
-
if (cacheService) {
|
|
657
|
-
cacheService.handleMessage(windowId, msg, (resp) => {
|
|
658
|
-
if (!subscriptions.has(subKey)) return;
|
|
659
|
-
hooks.sendToNapplet(windowId, normalizeRelayServiceResponse(resp));
|
|
660
|
-
});
|
|
661
|
-
}
|
|
698
|
+
relayService.handleMessage(windowId, msg, send);
|
|
699
|
+
if (cacheService && isActive()) cacheService.handleMessage(windowId, msg, send);
|
|
700
|
+
if (!isActive()) closeRelayBackends(context, windowId, subId, subKey);
|
|
662
701
|
return;
|
|
663
702
|
}
|
|
664
703
|
const relayHint = typeof m.relay === "string" && m.relay.length > 0 ? m.relay : void 0;
|
|
665
|
-
|
|
704
|
+
if (!isActive()) return;
|
|
705
|
+
deliverFromRuntimeBackends(context, subId, subKey, filters, isShellKind, deliver, send, isActive, relayHint);
|
|
666
706
|
}
|
|
667
|
-
function deliverFromRuntimeBackends(context,
|
|
707
|
+
function deliverFromRuntimeBackends(context, subId, subKey, filters, isShellKind, deliver, send, isActive, relayHint) {
|
|
668
708
|
const { hooks } = context;
|
|
669
709
|
const cache = hooks.cache;
|
|
670
710
|
if (cache?.isAvailable() && !isShellKind) {
|
|
@@ -675,7 +715,7 @@ function deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, i
|
|
|
675
715
|
}
|
|
676
716
|
const pool = hooks.relayPool;
|
|
677
717
|
if (!pool?.isAvailable() && !isShellKind) {
|
|
678
|
-
|
|
718
|
+
send({ type: "relay.eose", subId });
|
|
679
719
|
return;
|
|
680
720
|
}
|
|
681
721
|
if (!pool?.isAvailable() || isShellKind) return;
|
|
@@ -684,15 +724,16 @@ function deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, i
|
|
|
684
724
|
const eoseFallbackTimer = setTimeout(() => {
|
|
685
725
|
if (!eoseSent) {
|
|
686
726
|
eoseSent = true;
|
|
687
|
-
|
|
727
|
+
send({ type: "relay.eose", subId });
|
|
688
728
|
}
|
|
689
729
|
}, 15e3);
|
|
690
730
|
const subscription = pool.subscribe(filters, (item) => {
|
|
731
|
+
if (!isActive()) return;
|
|
691
732
|
if (item === "EOSE") {
|
|
692
733
|
clearTimeout(eoseFallbackTimer);
|
|
693
734
|
if (!eoseSent) {
|
|
694
735
|
eoseSent = true;
|
|
695
|
-
|
|
736
|
+
send({ type: "relay.eose", subId });
|
|
696
737
|
}
|
|
697
738
|
return;
|
|
698
739
|
}
|
|
@@ -710,6 +751,31 @@ function deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, i
|
|
|
710
751
|
clearTimeout(eoseFallbackTimer);
|
|
711
752
|
subscription.unsubscribe();
|
|
712
753
|
});
|
|
754
|
+
if (!isActive()) pool.untrackSubscription(subKey);
|
|
755
|
+
}
|
|
756
|
+
function closeRelayBackends(context, windowId, subId, subKey) {
|
|
757
|
+
const relay = context.serviceRegistry["relay"] ?? context.serviceRegistry["relay-pool"];
|
|
758
|
+
const cache = !context.serviceRegistry["relay"] ? context.serviceRegistry["cache"] : void 0;
|
|
759
|
+
for (const service of /* @__PURE__ */ new Set([relay, cache])) {
|
|
760
|
+
try {
|
|
761
|
+
service?.handleMessage(windowId, { type: "relay.close", subId }, () => {
|
|
762
|
+
});
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
context.hooks.relayPool?.untrackSubscription(subKey);
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function revokeRelaySubscriptions(context, windowId) {
|
|
772
|
+
const entries = [...context.subscriptions].filter(([, entry]) => entry.windowId === windowId);
|
|
773
|
+
for (const [key] of entries) context.subscriptions.delete(key);
|
|
774
|
+
for (const [key] of entries) {
|
|
775
|
+
const subId = key.slice(windowId.length + 1);
|
|
776
|
+
closeRelayBackends(context, windowId, subId, key);
|
|
777
|
+
context.hooks.sendToNapplet(windowId, { type: "relay.closed", subId, reason: "denied: relay:read" });
|
|
778
|
+
}
|
|
713
779
|
}
|
|
714
780
|
function handleRelayClose(context, windowId, msg, m) {
|
|
715
781
|
const { hooks, subscriptions } = context;
|
|
@@ -1850,13 +1916,18 @@ function buildObservation(envelope, windowId, senderCap, sessionRegistry, getFoc
|
|
|
1850
1916
|
}
|
|
1851
1917
|
function createFirewallGate(config) {
|
|
1852
1918
|
const { firewallState, sessionRegistry, hooks, fireConsent } = config;
|
|
1853
|
-
return function firewallGate(windowId, envelope, senderCap) {
|
|
1919
|
+
return function firewallGate(windowId, envelope, senderCap, respond = true) {
|
|
1854
1920
|
const obs = buildObservation(envelope, windowId, senderCap, sessionRegistry, hooks.getFocusContext);
|
|
1855
1921
|
const result = firewallState.evaluate(obs);
|
|
1856
1922
|
const { decision, action, ruleId, reason } = result;
|
|
1857
1923
|
const napplet = obs.napplet;
|
|
1858
1924
|
const opClass = obs.opClass;
|
|
1859
1925
|
if (decision === "reject" || decision === "prompt") {
|
|
1926
|
+
if (!respond) {
|
|
1927
|
+
hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
|
|
1928
|
+
if (decision === "prompt") fireConsent(windowId, napplet);
|
|
1929
|
+
return "drop";
|
|
1930
|
+
}
|
|
1860
1931
|
const intentDenial = createIntentPolicyDenial(envelope);
|
|
1861
1932
|
if (intentDenial) {
|
|
1862
1933
|
hooks.sendToNapplet(windowId, intentDenial);
|
|
@@ -1907,7 +1978,7 @@ function createDenialResponse(envelope, error) {
|
|
|
1907
1978
|
const type = denialResponseType(envelope);
|
|
1908
1979
|
return type ? { type, id, error } : null;
|
|
1909
1980
|
}
|
|
1910
|
-
function createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnvelope, firewallGate) {
|
|
1981
|
+
function createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnvelope, serviceRouting, firewallGate) {
|
|
1911
1982
|
return (windowId, msg) => {
|
|
1912
1983
|
if (typeof msg !== "object" || msg === null || Array.isArray(msg) || !("type" in msg) || typeof msg.type !== "string") return;
|
|
1913
1984
|
const envelope = msg;
|
|
@@ -1915,6 +1986,10 @@ function createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnv
|
|
|
1915
1986
|
if (dotIdx === -1) return;
|
|
1916
1987
|
if (!sessionRegistry.getEntryByWindowId(windowId)) return;
|
|
1917
1988
|
const domain = envelope.type.slice(0, dotIdx);
|
|
1989
|
+
if (!serviceRouting.builtInDomains.has(domain)) {
|
|
1990
|
+
serviceRouting.extensions.handleMessage(windowId, envelope);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1918
1993
|
if (hooks.isDomainAllowed && !hooks.isDomainAllowed(windowId, domain)) return;
|
|
1919
1994
|
if (isIdentityOrThemeMessage(envelope) && !createCanonicalDomainResult(envelope)) return;
|
|
1920
1995
|
const caps = resolveCapabilitiesNap(envelope);
|
|
@@ -1967,7 +2042,8 @@ function createRuntimeInstance(context) {
|
|
|
1967
2042
|
serviceRegistry,
|
|
1968
2043
|
sessionRegistry,
|
|
1969
2044
|
subscriptions,
|
|
1970
|
-
consentHandlerRef
|
|
2045
|
+
consentHandlerRef,
|
|
2046
|
+
extensions
|
|
1971
2047
|
} = context;
|
|
1972
2048
|
const undeclaredServiceConsents = /* @__PURE__ */ new Set();
|
|
1973
2049
|
const attachedServices = /* @__PURE__ */ new Map();
|
|
@@ -2031,6 +2107,8 @@ function createRuntimeInstance(context) {
|
|
|
2031
2107
|
eventBuffer.bufferAndDeliver(createInjectedEvent(hooks, topic, payload), null);
|
|
2032
2108
|
},
|
|
2033
2109
|
destroy() {
|
|
2110
|
+
extensions.destroy();
|
|
2111
|
+
registeredServices.clear();
|
|
2034
2112
|
for (const name of attachedServices.keys()) detachService(name);
|
|
2035
2113
|
manifestCache.persist();
|
|
2036
2114
|
aclState.persist();
|
|
@@ -2039,13 +2117,13 @@ function createRuntimeInstance(context) {
|
|
|
2039
2117
|
subscriptions.clear();
|
|
2040
2118
|
incRuntime.clear();
|
|
2041
2119
|
eventBuffer.clear();
|
|
2042
|
-
registeredServices.clear();
|
|
2043
2120
|
undeclaredServiceConsents.clear();
|
|
2044
2121
|
},
|
|
2045
2122
|
registerConsentHandler(handler) {
|
|
2046
2123
|
consentHandlerRef.current = handler;
|
|
2047
2124
|
},
|
|
2048
2125
|
registerService(name, handler) {
|
|
2126
|
+
registeredServices.delete(name);
|
|
2049
2127
|
detachService(name);
|
|
2050
2128
|
serviceRegistry[name] = handler;
|
|
2051
2129
|
registeredServices.set(name, {
|
|
@@ -2056,11 +2134,12 @@ function createRuntimeInstance(context) {
|
|
|
2056
2134
|
attachService(name, handler);
|
|
2057
2135
|
},
|
|
2058
2136
|
unregisterService(name) {
|
|
2137
|
+
registeredServices.delete(name);
|
|
2059
2138
|
detachService(name);
|
|
2060
2139
|
delete serviceRegistry[name];
|
|
2061
|
-
registeredServices.delete(name);
|
|
2062
2140
|
},
|
|
2063
2141
|
destroyWindow(windowId) {
|
|
2142
|
+
extensions.destroyWindow(windowId);
|
|
2064
2143
|
for (const [key] of subscriptions) {
|
|
2065
2144
|
if (key.startsWith(`${windowId}:`)) {
|
|
2066
2145
|
subscriptions.delete(key);
|
|
@@ -2089,14 +2168,17 @@ function createRuntime(hooks) {
|
|
|
2089
2168
|
const serviceRegistry = { ...hooks.services };
|
|
2090
2169
|
const registeredServices = createRegisteredServices(serviceRegistry);
|
|
2091
2170
|
const firewallState = createFirewallState(hooks.firewallPersistence);
|
|
2171
|
+
let extensions;
|
|
2092
2172
|
const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate, (windowId) => {
|
|
2093
2173
|
firewallState.resetInitBudget(windowId);
|
|
2174
|
+
extensions?.destroyWindow(windowId);
|
|
2094
2175
|
});
|
|
2095
2176
|
let incRuntime = null;
|
|
2096
2177
|
const aclState = createAclState(hooks.aclPersistence, "permissive", (mutation) => {
|
|
2097
2178
|
if (mutation.type === "revoke" && mutation.capability !== "relay:read") return;
|
|
2098
2179
|
for (const entry of sessionRegistry.getAllEntries()) {
|
|
2099
2180
|
if (entry.dTag === mutation.dTag && entry.aggregateHash === mutation.aggregateHash) {
|
|
2181
|
+
revokeRelaySubscriptions({ hooks, serviceRegistry, subscriptions }, entry.windowId);
|
|
2100
2182
|
incRuntime?.revokeWindow(entry.windowId);
|
|
2101
2183
|
}
|
|
2102
2184
|
}
|
|
@@ -2153,14 +2235,35 @@ function createRuntime(hooks) {
|
|
|
2153
2235
|
firewallState.load();
|
|
2154
2236
|
manifestCache.load();
|
|
2155
2237
|
const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry, aclState });
|
|
2156
|
-
const
|
|
2157
|
-
relay: createRelayHandler({
|
|
2238
|
+
const napHandlers = {
|
|
2239
|
+
relay: createRelayHandler({
|
|
2240
|
+
hooks,
|
|
2241
|
+
serviceRegistry,
|
|
2242
|
+
subscriptions,
|
|
2243
|
+
eventBuffer,
|
|
2244
|
+
replayDetector,
|
|
2245
|
+
canReadRelay(windowId) {
|
|
2246
|
+
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
2247
|
+
return !!entry && aclState.check("", entry.dTag, entry.aggregateHash, "relay:read");
|
|
2248
|
+
}
|
|
2249
|
+
}),
|
|
2158
2250
|
identity: createIdentityHandler({ hooks, serviceRegistry }),
|
|
2159
2251
|
count: createCountHandler({ hooks, serviceRegistry }),
|
|
2160
2252
|
inc: incRuntime.handleMessage,
|
|
2161
2253
|
...domainHandlers
|
|
2254
|
+
};
|
|
2255
|
+
const dispatchNapEnvelope = createNapEnvelopeDispatcher(napHandlers);
|
|
2256
|
+
const builtInDomains = /* @__PURE__ */ new Set([...Object.keys(napHandlers), "shell", "signer"]);
|
|
2257
|
+
extensions = createServiceExtensionRouter({
|
|
2258
|
+
hooks,
|
|
2259
|
+
services: serviceRegistry,
|
|
2260
|
+
registrations: registeredServices,
|
|
2261
|
+
sessions: sessionRegistry,
|
|
2262
|
+
acl: aclState,
|
|
2263
|
+
enforce: enforceNap,
|
|
2264
|
+
firewall: firewallGate
|
|
2162
2265
|
});
|
|
2163
|
-
const handleMessage = createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnvelope, firewallGate);
|
|
2266
|
+
const handleMessage = createMessageHandler(hooks, sessionRegistry, enforceNap, dispatchNapEnvelope, { builtInDomains, extensions }, firewallGate);
|
|
2164
2267
|
return createRuntimeInstance({
|
|
2165
2268
|
hooks,
|
|
2166
2269
|
serviceRegistry,
|
|
@@ -2174,14 +2277,15 @@ function createRuntime(hooks) {
|
|
|
2174
2277
|
firewallState,
|
|
2175
2278
|
manifestCache,
|
|
2176
2279
|
consentHandlerRef,
|
|
2177
|
-
handleMessage
|
|
2280
|
+
handleMessage,
|
|
2281
|
+
extensions
|
|
2178
2282
|
});
|
|
2179
2283
|
}
|
|
2180
2284
|
|
|
2181
2285
|
// src/index.ts
|
|
2182
|
-
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
2286
|
+
import { ALL_CAPABILITIES as ALL_CAPABILITIES2 } from "@kehto/acl/capabilities";
|
|
2183
2287
|
export {
|
|
2184
|
-
ALL_CAPABILITIES,
|
|
2288
|
+
ALL_CAPABILITIES2 as ALL_CAPABILITIES,
|
|
2185
2289
|
RING_BUFFER_SIZE,
|
|
2186
2290
|
cleanupNappState,
|
|
2187
2291
|
createAclState,
|