@kehto/runtime 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { resolveCapabilitiesNub } from "@kehto/acl";
4
4
  var CLASS_CAPABILITY_ALLOWLIST = Object.freeze({
5
5
  "class-1": new Set(ALL_CAPABILITIES),
6
6
  "class-2": new Set(ALL_CAPABILITIES.filter(
7
- (c) => c !== "relay:write" && c !== "identity:decrypt" && c !== "outbox:write"
7
+ (c) => c !== "relay:write" && c !== "outbox:write" && c !== "intent:write"
8
8
  ))
9
9
  });
10
10
  function createEnforceGate(config) {
@@ -154,11 +154,12 @@ var CAP_NOTIFY_CHANNEL = 1 << 12;
154
154
  var CAP_THEME_READ = 1 << 13;
155
155
  var CAP_CONFIG_READ = 1 << 14;
156
156
  var CAP_RESOURCE_FETCH = 1 << 15;
157
- var CAP_IDENTITY_DECRYPT = 1 << 16;
158
157
  var CAP_CVM_CALL = 1 << 17;
159
158
  var CAP_OUTBOX_READ = 1 << 18;
160
159
  var CAP_OUTBOX_WRITE = 1 << 19;
161
160
  var CAP_UPLOAD_WRITE = 1 << 20;
161
+ var CAP_INTENT_READ = 1 << 21;
162
+ var CAP_INTENT_WRITE = 1 << 22;
162
163
  var CAP_MAP = {
163
164
  "relay:read": CAP_RELAY_READ,
164
165
  "relay:write": CAP_RELAY_WRITE,
@@ -176,11 +177,12 @@ var CAP_MAP = {
176
177
  "theme:read": CAP_THEME_READ,
177
178
  "config:read": CAP_CONFIG_READ,
178
179
  "resource:fetch": CAP_RESOURCE_FETCH,
179
- "identity:decrypt": CAP_IDENTITY_DECRYPT,
180
180
  "cvm:call": CAP_CVM_CALL,
181
181
  "outbox:read": CAP_OUTBOX_READ,
182
182
  "outbox:write": CAP_OUTBOX_WRITE,
183
- "upload:write": CAP_UPLOAD_WRITE
183
+ "upload:write": CAP_UPLOAD_WRITE,
184
+ "intent:read": CAP_INTENT_READ,
185
+ "intent:write": CAP_INTENT_WRITE
184
186
  };
185
187
  var RUNTIME_CAP_ALL = Object.values(CAP_MAP).reduce((bits, bit) => bits | bit, 0);
186
188
  function capToBit(cap) {
@@ -283,6 +285,68 @@ function createAclState(persistence, defaultPolicy = "permissive") {
283
285
  };
284
286
  }
285
287
 
288
+ // src/firewall-state.ts
289
+ import {
290
+ evaluate,
291
+ defaultConfig,
292
+ createState as createState2,
293
+ serialize as serialize2,
294
+ deserialize as deserialize2,
295
+ setPolicy,
296
+ setRateLimit,
297
+ setGlobalRate,
298
+ addMatcher
299
+ } from "@kehto/firewall";
300
+ function createFirewallState(persistence) {
301
+ let config = defaultConfig();
302
+ let counters = createState2();
303
+ return {
304
+ evaluate(observation) {
305
+ const result = evaluate(config, counters, observation);
306
+ counters = result.newState;
307
+ return result;
308
+ },
309
+ setPolicy(napplet, policy) {
310
+ config = setPolicy(config, napplet, policy);
311
+ },
312
+ setRateLimit(napplet, opClass, limit) {
313
+ config = setRateLimit(config, napplet, opClass, limit);
314
+ },
315
+ setGlobalRate(napplet, limit) {
316
+ config = setGlobalRate(config, napplet, limit);
317
+ },
318
+ addMatcher(matcher) {
319
+ config = addMatcher(config, matcher);
320
+ },
321
+ getConfig() {
322
+ return config;
323
+ },
324
+ persist() {
325
+ try {
326
+ persistence?.persist(serialize2(config));
327
+ } catch {
328
+ }
329
+ },
330
+ load() {
331
+ try {
332
+ const raw = persistence?.load() ?? null;
333
+ if (!raw) return;
334
+ config = deserialize2(raw);
335
+ } catch {
336
+ config = defaultConfig();
337
+ }
338
+ },
339
+ clear() {
340
+ config = defaultConfig();
341
+ counters = createState2();
342
+ try {
343
+ persistence?.persist("");
344
+ } catch {
345
+ }
346
+ }
347
+ };
348
+ }
349
+
286
350
  // src/manifest-cache.ts
287
351
  function createManifestCache(persistence) {
288
352
  const cache = /* @__PURE__ */ new Map();
@@ -778,7 +842,8 @@ function createIfcRuntime(hooks, sessionRegistry) {
778
842
  const state = {
779
843
  subscriptions: /* @__PURE__ */ new Map(),
780
844
  channels: /* @__PURE__ */ new Map(),
781
- channelsByWindow: /* @__PURE__ */ new Map()
845
+ channelsByWindow: /* @__PURE__ */ new Map(),
846
+ domainByWindow: /* @__PURE__ */ new Map()
782
847
  };
783
848
  return {
784
849
  handleMessage(windowId, msg) {
@@ -787,14 +852,24 @@ function createIfcRuntime(hooks, sessionRegistry) {
787
852
  destroyWindow(windowId) {
788
853
  removeWindowChannels(state, hooks, windowId);
789
854
  removeWindowSubscriptions(state, windowId);
855
+ state.domainByWindow.delete(windowId);
790
856
  },
791
857
  clear() {
792
858
  state.subscriptions.clear();
793
859
  state.channels.clear();
794
860
  state.channelsByWindow.clear();
861
+ state.domainByWindow.clear();
795
862
  }
796
863
  };
797
864
  }
865
+ function domainOf(type) {
866
+ const dot = type.indexOf(".");
867
+ const d = dot === -1 ? type : type.slice(0, dot);
868
+ return d === "inc" ? "inc" : "ifc";
869
+ }
870
+ function prefixFor(state, windowId, fallback) {
871
+ return state.domainByWindow.get(windowId) ?? fallback;
872
+ }
798
873
  function addChannel(state, channelId, peerA, peerB) {
799
874
  state.channels.set(channelId, { channelId, peerA, peerB });
800
875
  for (const windowId of [peerA, peerB]) {
@@ -834,51 +909,56 @@ function handleIfcMessage(state, hooks, sessionRegistry, windowId, msg) {
834
909
  const m = msg;
835
910
  const dotIdx = msg.type.indexOf(".");
836
911
  const action = msg.type.slice(dotIdx + 1);
912
+ const incomingDomain = domainOf(msg.type);
913
+ if (!state.domainByWindow.has(windowId)) {
914
+ state.domainByWindow.set(windowId, incomingDomain);
915
+ }
837
916
  switch (action) {
838
917
  case "emit":
839
- handleEmit(state, hooks, windowId, m);
918
+ handleEmit(state, hooks, windowId, m, incomingDomain);
840
919
  return;
841
920
  case "subscribe":
842
- handleSubscribe(state, hooks, windowId, m);
921
+ handleSubscribe(state, hooks, windowId, m, incomingDomain);
843
922
  return;
844
923
  case "unsubscribe":
845
924
  handleUnsubscribe(state, windowId, m);
846
925
  return;
847
926
  case "channel.open":
848
- handleChannelOpen(state, hooks, sessionRegistry, windowId, m);
927
+ handleChannelOpen(state, hooks, sessionRegistry, windowId, m, incomingDomain);
849
928
  return;
850
929
  case "channel.emit":
851
- handleChannelEmit(state, hooks, windowId, m);
930
+ handleChannelEmit(state, hooks, windowId, m, incomingDomain);
852
931
  return;
853
932
  case "channel.broadcast":
854
- handleChannelBroadcast(state, hooks, windowId, m);
933
+ handleChannelBroadcast(state, hooks, windowId, m, incomingDomain);
855
934
  return;
856
935
  case "channel.list":
857
- handleChannelList(state, hooks, windowId, m);
936
+ handleChannelList(state, hooks, windowId, m, incomingDomain);
858
937
  return;
859
938
  case "channel.close":
860
- handleChannelClose(state, hooks, windowId, m);
939
+ handleChannelClose(state, hooks, windowId, m, incomingDomain);
861
940
  return;
862
941
  default:
863
942
  return;
864
943
  }
865
944
  }
866
- function handleEmit(state, hooks, windowId, m) {
945
+ function handleEmit(state, hooks, windowId, m, senderDomain) {
867
946
  const topic = m.topic ?? "";
868
947
  if (!topic) return;
869
948
  const subscribers = state.subscriptions.get(topic);
870
949
  if (!subscribers) return;
871
950
  for (const subscriberWindowId of subscribers) {
872
951
  if (subscriberWindowId !== windowId) {
873
- hooks.sendToNapplet(subscriberWindowId, { type: "ifc.event", topic, payload: m.payload, sender: windowId });
952
+ const prefix = prefixFor(state, subscriberWindowId, senderDomain);
953
+ hooks.sendToNapplet(subscriberWindowId, { type: `${prefix}.event`, topic, payload: m.payload, sender: windowId });
874
954
  }
875
955
  }
876
956
  }
877
- function handleSubscribe(state, hooks, windowId, m) {
957
+ function handleSubscribe(state, hooks, windowId, m, incomingDomain) {
878
958
  const id = m.id ?? "";
879
959
  const topic = m.topic ?? "";
880
960
  if (!topic) {
881
- hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id, error: "missing topic" });
961
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.subscribe.result`, id, error: "missing topic" });
882
962
  return;
883
963
  }
884
964
  let subscriptions = state.subscriptions.get(topic);
@@ -887,7 +967,7 @@ function handleSubscribe(state, hooks, windowId, m) {
887
967
  state.subscriptions.set(topic, subscriptions);
888
968
  }
889
969
  subscriptions.add(windowId);
890
- hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id });
970
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.subscribe.result`, id });
891
971
  }
892
972
  function handleUnsubscribe(state, windowId, m) {
893
973
  const topic = m.topic ?? "";
@@ -897,30 +977,36 @@ function handleUnsubscribe(state, windowId, m) {
897
977
  subscriptions.delete(windowId);
898
978
  if (subscriptions.size === 0) state.subscriptions.delete(topic);
899
979
  }
900
- function handleChannelOpen(state, hooks, sessionRegistry, windowId, m) {
980
+ function handleChannelOpen(state, hooks, sessionRegistry, windowId, m, incomingDomain) {
901
981
  const id = m.id ?? "";
902
982
  const peerWindow = resolveTarget(sessionRegistry, m.target ?? "");
903
983
  if (!peerWindow) {
904
- hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, error: "target not found" });
984
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.channel.open.result`, id, error: "target not found" });
905
985
  return;
906
986
  }
907
987
  const channelId = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
908
988
  addChannel(state, channelId, windowId, peerWindow);
909
- hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, channelId, peer: peerWindow });
989
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.channel.open.result`, id, channelId, peer: peerWindow });
910
990
  }
911
- function handleChannelEmit(state, hooks, windowId, m) {
991
+ function handleChannelEmit(state, hooks, windowId, m, senderDomain) {
912
992
  const peer = peerOf(state, m.channelId ?? "", windowId);
913
- if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId: m.channelId ?? "", sender: windowId, payload: m.payload });
993
+ if (peer) {
994
+ const prefix = prefixFor(state, peer, senderDomain);
995
+ hooks.sendToNapplet(peer, { type: `${prefix}.channel.event`, channelId: m.channelId ?? "", sender: windowId, payload: m.payload });
996
+ }
914
997
  }
915
- function handleChannelBroadcast(state, hooks, windowId, m) {
998
+ function handleChannelBroadcast(state, hooks, windowId, m, senderDomain) {
916
999
  const channels = state.channelsByWindow.get(windowId);
917
1000
  if (!channels) return;
918
1001
  for (const channelId of channels) {
919
1002
  const peer = peerOf(state, channelId, windowId);
920
- if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
1003
+ if (peer) {
1004
+ const prefix = prefixFor(state, peer, senderDomain);
1005
+ hooks.sendToNapplet(peer, { type: `${prefix}.channel.event`, channelId, sender: windowId, payload: m.payload });
1006
+ }
921
1007
  }
922
1008
  }
923
- function handleChannelList(state, hooks, windowId, m) {
1009
+ function handleChannelList(state, hooks, windowId, m, incomingDomain) {
924
1010
  const channels = [];
925
1011
  const set = state.channelsByWindow.get(windowId);
926
1012
  if (set) {
@@ -929,14 +1015,15 @@ function handleChannelList(state, hooks, windowId, m) {
929
1015
  if (peer) channels.push({ id: channelId, peer });
930
1016
  }
931
1017
  }
932
- hooks.sendToNapplet(windowId, { type: "ifc.channel.list.result", id: m.id ?? "", channels });
1018
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.channel.list.result`, id: m.id ?? "", channels });
933
1019
  }
934
- function handleChannelClose(state, hooks, windowId, m) {
1020
+ function handleChannelClose(state, hooks, windowId, m, closerDomain) {
935
1021
  const channelId = m.channelId ?? "";
936
1022
  const peer = peerOf(state, channelId, windowId);
937
1023
  if (!peer) return;
938
- hooks.sendToNapplet(windowId, { type: "ifc.channel.closed", channelId });
939
- hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
1024
+ hooks.sendToNapplet(windowId, { type: `${closerDomain}.channel.closed`, channelId });
1025
+ const peerPrefix = prefixFor(state, peer, closerDomain);
1026
+ hooks.sendToNapplet(peer, { type: `${peerPrefix}.channel.closed`, channelId });
940
1027
  removeChannel(state, channelId);
941
1028
  }
942
1029
  function removeWindowSubscriptions(state, windowId) {
@@ -950,7 +1037,11 @@ function removeWindowChannels(state, hooks, windowId) {
950
1037
  if (!channelIds) return;
951
1038
  for (const channelId of Array.from(channelIds)) {
952
1039
  const peer = peerOf(state, channelId, windowId);
953
- if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
1040
+ if (peer) {
1041
+ const destroyeeDomain = prefixFor(state, windowId, "ifc");
1042
+ const peerPrefix = prefixFor(state, peer, destroyeeDomain);
1043
+ hooks.sendToNapplet(peer, { type: `${peerPrefix}.channel.closed`, channelId });
1044
+ }
954
1045
  removeChannel(state, channelId);
955
1046
  }
956
1047
  }
@@ -1082,7 +1173,8 @@ function createRuntimeDomainHandlers(context) {
1082
1173
  resource: (windowId, msg) => handleServiceOnlyMessage(context, "resource", windowId, msg),
1083
1174
  cvm: (windowId, msg) => handleServiceOnlyMessage(context, "cvm", windowId, msg),
1084
1175
  outbox: (windowId, msg) => handleServiceOnlyMessage(context, "outbox", windowId, msg),
1085
- upload: (windowId, msg) => handleServiceOnlyMessage(context, "upload", windowId, msg)
1176
+ upload: (windowId, msg) => handleServiceOnlyMessage(context, "upload", windowId, msg),
1177
+ intent: (windowId, msg) => handleServiceOnlyMessage(context, "intent", windowId, msg)
1086
1178
  };
1087
1179
  }
1088
1180
  function handleStorageMessage(context, windowId, msg) {
@@ -1218,12 +1310,14 @@ function createNubEnvelopeDispatcher(handlers) {
1218
1310
  nubDispatch.registerNub("notify", adapt(handlers.notify));
1219
1311
  nubDispatch.registerNub("storage", adapt(handlers.storage));
1220
1312
  nubDispatch.registerNub("ifc", adapt(handlers.ifc));
1313
+ nubDispatch.registerNub("inc", adapt(handlers.ifc));
1221
1314
  nubDispatch.registerNub("theme", adapt(handlers.theme));
1222
1315
  nubDispatch.registerNub("config", adapt(handlers.config));
1223
1316
  nubDispatch.registerNub("resource", adapt(handlers.resource));
1224
1317
  nubDispatch.registerNub("cvm", adapt(handlers.cvm));
1225
1318
  nubDispatch.registerNub("outbox", adapt(handlers.outbox));
1226
1319
  nubDispatch.registerNub("upload", adapt(handlers.upload));
1320
+ nubDispatch.registerNub("intent", adapt(handlers.intent));
1227
1321
  return (windowId, envelope) => {
1228
1322
  currentWindowId = windowId;
1229
1323
  try {
@@ -1233,7 +1327,67 @@ function createNubEnvelopeDispatcher(handlers) {
1233
1327
  }
1234
1328
  };
1235
1329
  }
1236
- function createMessageHandler(hooks, enforceNub, dispatchNubEnvelope) {
1330
+ function utf8ByteLength(str) {
1331
+ let bytes = 0;
1332
+ for (let i = 0; i < str.length; i++) {
1333
+ const c = str.charCodeAt(i);
1334
+ if (c < 128) bytes += 1;
1335
+ else if (c < 2048) bytes += 2;
1336
+ else if (c < 55296 || c >= 57344) bytes += 3;
1337
+ else {
1338
+ i++;
1339
+ bytes += 4;
1340
+ }
1341
+ }
1342
+ return bytes;
1343
+ }
1344
+ function extractKindSize(envelope) {
1345
+ const ev = envelope.event;
1346
+ if (typeof ev !== "object" || ev === null) return {};
1347
+ const kind = typeof ev.kind === "number" ? ev.kind : void 0;
1348
+ let size;
1349
+ try {
1350
+ size = utf8ByteLength(JSON.stringify(ev));
1351
+ } catch {
1352
+ }
1353
+ return { kind, size };
1354
+ }
1355
+ function buildObservation(envelope, windowId, senderCap, sessionRegistry, getFocusContext) {
1356
+ const now = Date.now();
1357
+ const entry = sessionRegistry.getEntryByWindowId(windowId);
1358
+ const napplet = entry?.dTag ?? "";
1359
+ const initElapsedMs = now - (entry?.registeredAt ?? now);
1360
+ const focus = getFocusContext?.(windowId) ?? { focused: true };
1361
+ const opClass = senderCap ?? envelope.type;
1362
+ const { kind, size } = extractKindSize(envelope);
1363
+ return { napplet, opClass, kind, size, initElapsedMs, focused: focus.focused, msSinceFocusGain: focus.msSinceFocusGain, now };
1364
+ }
1365
+ function createFirewallGate(config) {
1366
+ const { firewallState, sessionRegistry, hooks, fireConsent } = config;
1367
+ return function firewallGate(windowId, envelope, senderCap) {
1368
+ const obs = buildObservation(envelope, windowId, senderCap, sessionRegistry, hooks.getFocusContext);
1369
+ const result = firewallState.evaluate(obs);
1370
+ const { decision, action, ruleId, reason } = result;
1371
+ const napplet = obs.napplet;
1372
+ const opClass = obs.opClass;
1373
+ if (decision === "reject" || decision === "prompt") {
1374
+ const id = envelope.id ?? "";
1375
+ const isStorageEnvelope = envelope.type.startsWith("storage.");
1376
+ const type = isStorageEnvelope ? `${envelope.type}.result` : `${envelope.type}.error`;
1377
+ hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
1378
+ hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
1379
+ if (decision === "prompt") {
1380
+ fireConsent(windowId, napplet);
1381
+ }
1382
+ return "drop";
1383
+ }
1384
+ if (action === "flag") {
1385
+ hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
1386
+ }
1387
+ return "dispatch";
1388
+ };
1389
+ }
1390
+ function createMessageHandler(hooks, enforceNub, dispatchNubEnvelope, firewallGate) {
1237
1391
  return (windowId, msg) => {
1238
1392
  if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
1239
1393
  const envelope = msg;
@@ -1244,14 +1398,15 @@ function createMessageHandler(hooks, enforceNub, dispatchNubEnvelope) {
1244
1398
  const result = enforceNub(windowId, caps.senderCap, envelope);
1245
1399
  if (!result.allowed) {
1246
1400
  const id = envelope.id ?? "";
1247
- const isIdentityDecrypt = envelope.type === "identity.decrypt";
1248
1401
  const isStorageEnvelope = envelope.type.startsWith("storage.");
1249
- const error = isIdentityDecrypt ? result.reason === "class-forbidden" ? "class-forbidden" : "policy-denied" : formatDenialReason(result.capability);
1402
+ const error = formatDenialReason(result.capability);
1250
1403
  const type = isStorageEnvelope ? `${envelope.type}.result` : `${envelope.type}.error`;
1251
1404
  hooks.sendToNapplet(windowId, { type, id, error });
1252
1405
  return;
1253
1406
  }
1254
1407
  }
1408
+ const verdict = firewallGate(windowId, envelope, caps.senderCap);
1409
+ if (verdict === "drop") return;
1255
1410
  dispatchNubEnvelope(windowId, envelope);
1256
1411
  };
1257
1412
  }
@@ -1270,6 +1425,7 @@ function createInjectedEvent(hooks, topic, payload) {
1270
1425
  function createRuntimeInstance(context) {
1271
1426
  const {
1272
1427
  aclState,
1428
+ firewallState,
1273
1429
  eventBuffer,
1274
1430
  hooks,
1275
1431
  ifcRuntime,
@@ -1278,10 +1434,10 @@ function createRuntimeInstance(context) {
1278
1434
  replayDetector,
1279
1435
  serviceRegistry,
1280
1436
  sessionRegistry,
1281
- subscriptions
1437
+ subscriptions,
1438
+ consentHandlerRef
1282
1439
  } = context;
1283
1440
  const undeclaredServiceConsents = /* @__PURE__ */ new Set();
1284
- let consentHandler = null;
1285
1441
  return {
1286
1442
  handleMessage: context.handleMessage,
1287
1443
  injectEvent(topic, payload) {
@@ -1290,6 +1446,7 @@ function createRuntimeInstance(context) {
1290
1446
  destroy() {
1291
1447
  manifestCache.persist();
1292
1448
  aclState.persist();
1449
+ firewallState.persist();
1293
1450
  replayDetector.clear();
1294
1451
  subscriptions.clear();
1295
1452
  ifcRuntime.clear();
@@ -1298,8 +1455,7 @@ function createRuntimeInstance(context) {
1298
1455
  undeclaredServiceConsents.clear();
1299
1456
  },
1300
1457
  registerConsentHandler(handler) {
1301
- consentHandler = handler;
1302
- void consentHandler;
1458
+ consentHandlerRef.current = handler;
1303
1459
  },
1304
1460
  registerService(name, handler) {
1305
1461
  serviceRegistry[name] = handler;
@@ -1329,6 +1485,9 @@ function createRuntimeInstance(context) {
1329
1485
  get aclState() {
1330
1486
  return aclState;
1331
1487
  },
1488
+ get firewallState() {
1489
+ return firewallState;
1490
+ },
1332
1491
  get manifestCache() {
1333
1492
  return manifestCache;
1334
1493
  }
@@ -1340,10 +1499,26 @@ function createRuntime(hooks) {
1340
1499
  const registeredServices = createRegisteredServices(serviceRegistry);
1341
1500
  const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate);
1342
1501
  const aclState = createAclState(hooks.aclPersistence);
1502
+ const firewallState = createFirewallState(hooks.firewallPersistence);
1343
1503
  const manifestCache = createManifestCache(hooks.manifestPersistence);
1344
1504
  const replayDetector = createReplayDetector(
1345
1505
  hooks.getConfigOverrides ? () => hooks.getConfigOverrides().replayWindowSeconds : void 0
1346
1506
  );
1507
+ const consentHandlerRef = { current: null };
1508
+ const fireConsent = (windowId, napplet) => {
1509
+ const handler = consentHandlerRef.current;
1510
+ if (!handler) return;
1511
+ handler({
1512
+ type: "firewall-policy",
1513
+ windowId,
1514
+ napplet,
1515
+ pubkey: "",
1516
+ resolve: (allowed) => {
1517
+ firewallState.setPolicy(napplet, allowed ? "allow" : "deny");
1518
+ firewallState.persist();
1519
+ }
1520
+ });
1521
+ };
1347
1522
  const enforce = createEnforceGate({
1348
1523
  checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
1349
1524
  resolveIdentity: (pubkey) => {
@@ -1360,6 +1535,7 @@ function createRuntime(hooks) {
1360
1535
  },
1361
1536
  onAclCheck: hooks.onAclCheck
1362
1537
  });
1538
+ const firewallGate = createFirewallGate({ firewallState, sessionRegistry, hooks, fireConsent });
1363
1539
  const eventBuffer = createEventBuffer(
1364
1540
  hooks.sendToNapplet,
1365
1541
  sessionRegistry,
@@ -1368,6 +1544,7 @@ function createRuntime(hooks) {
1368
1544
  hooks.getConfigOverrides ? () => hooks.getConfigOverrides().ringBufferSize ?? RING_BUFFER_SIZE : void 0
1369
1545
  );
1370
1546
  aclState.load();
1547
+ firewallState.load();
1371
1548
  manifestCache.load();
1372
1549
  const ifcRuntime = createIfcRuntime(hooks, sessionRegistry);
1373
1550
  const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry, aclState });
@@ -1377,7 +1554,7 @@ function createRuntime(hooks) {
1377
1554
  ifc: ifcRuntime.handleMessage,
1378
1555
  ...domainHandlers
1379
1556
  });
1380
- const handleMessage = createMessageHandler(hooks, enforceNub, dispatchNubEnvelope);
1557
+ const handleMessage = createMessageHandler(hooks, enforceNub, dispatchNubEnvelope, firewallGate);
1381
1558
  return createRuntimeInstance({
1382
1559
  hooks,
1383
1560
  serviceRegistry,
@@ -1388,7 +1565,9 @@ function createRuntime(hooks) {
1388
1565
  ifcRuntime,
1389
1566
  sessionRegistry,
1390
1567
  aclState,
1568
+ firewallState,
1391
1569
  manifestCache,
1570
+ consentHandlerRef,
1392
1571
  handleMessage
1393
1572
  });
1394
1573
  }
@@ -1402,6 +1581,7 @@ export {
1402
1581
  createAclState,
1403
1582
  createEnforceGate,
1404
1583
  createEventBuffer,
1584
+ createFirewallState,
1405
1585
  createManifestCache,
1406
1586
  createNappKeyRegistry,
1407
1587
  createNubEnforceGate,