@kehto/runtime 0.8.0 → 0.11.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 !== "outbox:write"
7
+ (c) => c !== "relay:write" && c !== "outbox:write" && c !== "intent:write"
8
8
  ))
9
9
  });
10
10
  function createEnforceGate(config) {
@@ -158,6 +158,8 @@ var CAP_CVM_CALL = 1 << 17;
158
158
  var CAP_OUTBOX_READ = 1 << 18;
159
159
  var CAP_OUTBOX_WRITE = 1 << 19;
160
160
  var CAP_UPLOAD_WRITE = 1 << 20;
161
+ var CAP_INTENT_READ = 1 << 21;
162
+ var CAP_INTENT_WRITE = 1 << 22;
161
163
  var CAP_MAP = {
162
164
  "relay:read": CAP_RELAY_READ,
163
165
  "relay:write": CAP_RELAY_WRITE,
@@ -178,7 +180,9 @@ var CAP_MAP = {
178
180
  "cvm:call": CAP_CVM_CALL,
179
181
  "outbox:read": CAP_OUTBOX_READ,
180
182
  "outbox:write": CAP_OUTBOX_WRITE,
181
- "upload:write": CAP_UPLOAD_WRITE
183
+ "upload:write": CAP_UPLOAD_WRITE,
184
+ "intent:read": CAP_INTENT_READ,
185
+ "intent:write": CAP_INTENT_WRITE
182
186
  };
183
187
  var RUNTIME_CAP_ALL = Object.values(CAP_MAP).reduce((bits, bit) => bits | bit, 0);
184
188
  function capToBit(cap) {
@@ -281,6 +285,68 @@ function createAclState(persistence, defaultPolicy = "permissive") {
281
285
  };
282
286
  }
283
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
+
284
350
  // src/manifest-cache.ts
285
351
  function createManifestCache(persistence) {
286
352
  const cache = /* @__PURE__ */ new Map();
@@ -776,7 +842,8 @@ function createIfcRuntime(hooks, sessionRegistry) {
776
842
  const state = {
777
843
  subscriptions: /* @__PURE__ */ new Map(),
778
844
  channels: /* @__PURE__ */ new Map(),
779
- channelsByWindow: /* @__PURE__ */ new Map()
845
+ channelsByWindow: /* @__PURE__ */ new Map(),
846
+ domainByWindow: /* @__PURE__ */ new Map()
780
847
  };
781
848
  return {
782
849
  handleMessage(windowId, msg) {
@@ -785,14 +852,24 @@ function createIfcRuntime(hooks, sessionRegistry) {
785
852
  destroyWindow(windowId) {
786
853
  removeWindowChannels(state, hooks, windowId);
787
854
  removeWindowSubscriptions(state, windowId);
855
+ state.domainByWindow.delete(windowId);
788
856
  },
789
857
  clear() {
790
858
  state.subscriptions.clear();
791
859
  state.channels.clear();
792
860
  state.channelsByWindow.clear();
861
+ state.domainByWindow.clear();
793
862
  }
794
863
  };
795
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
+ }
796
873
  function addChannel(state, channelId, peerA, peerB) {
797
874
  state.channels.set(channelId, { channelId, peerA, peerB });
798
875
  for (const windowId of [peerA, peerB]) {
@@ -832,51 +909,56 @@ function handleIfcMessage(state, hooks, sessionRegistry, windowId, msg) {
832
909
  const m = msg;
833
910
  const dotIdx = msg.type.indexOf(".");
834
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
+ }
835
916
  switch (action) {
836
917
  case "emit":
837
- handleEmit(state, hooks, windowId, m);
918
+ handleEmit(state, hooks, windowId, m, incomingDomain);
838
919
  return;
839
920
  case "subscribe":
840
- handleSubscribe(state, hooks, windowId, m);
921
+ handleSubscribe(state, hooks, windowId, m, incomingDomain);
841
922
  return;
842
923
  case "unsubscribe":
843
924
  handleUnsubscribe(state, windowId, m);
844
925
  return;
845
926
  case "channel.open":
846
- handleChannelOpen(state, hooks, sessionRegistry, windowId, m);
927
+ handleChannelOpen(state, hooks, sessionRegistry, windowId, m, incomingDomain);
847
928
  return;
848
929
  case "channel.emit":
849
- handleChannelEmit(state, hooks, windowId, m);
930
+ handleChannelEmit(state, hooks, windowId, m, incomingDomain);
850
931
  return;
851
932
  case "channel.broadcast":
852
- handleChannelBroadcast(state, hooks, windowId, m);
933
+ handleChannelBroadcast(state, hooks, windowId, m, incomingDomain);
853
934
  return;
854
935
  case "channel.list":
855
- handleChannelList(state, hooks, windowId, m);
936
+ handleChannelList(state, hooks, windowId, m, incomingDomain);
856
937
  return;
857
938
  case "channel.close":
858
- handleChannelClose(state, hooks, windowId, m);
939
+ handleChannelClose(state, hooks, windowId, m, incomingDomain);
859
940
  return;
860
941
  default:
861
942
  return;
862
943
  }
863
944
  }
864
- function handleEmit(state, hooks, windowId, m) {
945
+ function handleEmit(state, hooks, windowId, m, senderDomain) {
865
946
  const topic = m.topic ?? "";
866
947
  if (!topic) return;
867
948
  const subscribers = state.subscriptions.get(topic);
868
949
  if (!subscribers) return;
869
950
  for (const subscriberWindowId of subscribers) {
870
951
  if (subscriberWindowId !== windowId) {
871
- 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 });
872
954
  }
873
955
  }
874
956
  }
875
- function handleSubscribe(state, hooks, windowId, m) {
957
+ function handleSubscribe(state, hooks, windowId, m, incomingDomain) {
876
958
  const id = m.id ?? "";
877
959
  const topic = m.topic ?? "";
878
960
  if (!topic) {
879
- hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id, error: "missing topic" });
961
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.subscribe.result`, id, error: "missing topic" });
880
962
  return;
881
963
  }
882
964
  let subscriptions = state.subscriptions.get(topic);
@@ -885,7 +967,7 @@ function handleSubscribe(state, hooks, windowId, m) {
885
967
  state.subscriptions.set(topic, subscriptions);
886
968
  }
887
969
  subscriptions.add(windowId);
888
- hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id });
970
+ hooks.sendToNapplet(windowId, { type: `${incomingDomain}.subscribe.result`, id });
889
971
  }
890
972
  function handleUnsubscribe(state, windowId, m) {
891
973
  const topic = m.topic ?? "";
@@ -895,30 +977,36 @@ function handleUnsubscribe(state, windowId, m) {
895
977
  subscriptions.delete(windowId);
896
978
  if (subscriptions.size === 0) state.subscriptions.delete(topic);
897
979
  }
898
- function handleChannelOpen(state, hooks, sessionRegistry, windowId, m) {
980
+ function handleChannelOpen(state, hooks, sessionRegistry, windowId, m, incomingDomain) {
899
981
  const id = m.id ?? "";
900
982
  const peerWindow = resolveTarget(sessionRegistry, m.target ?? "");
901
983
  if (!peerWindow) {
902
- 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" });
903
985
  return;
904
986
  }
905
987
  const channelId = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
906
988
  addChannel(state, channelId, windowId, peerWindow);
907
- 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 });
908
990
  }
909
- function handleChannelEmit(state, hooks, windowId, m) {
991
+ function handleChannelEmit(state, hooks, windowId, m, senderDomain) {
910
992
  const peer = peerOf(state, m.channelId ?? "", windowId);
911
- 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
+ }
912
997
  }
913
- function handleChannelBroadcast(state, hooks, windowId, m) {
998
+ function handleChannelBroadcast(state, hooks, windowId, m, senderDomain) {
914
999
  const channels = state.channelsByWindow.get(windowId);
915
1000
  if (!channels) return;
916
1001
  for (const channelId of channels) {
917
1002
  const peer = peerOf(state, channelId, windowId);
918
- 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
+ }
919
1007
  }
920
1008
  }
921
- function handleChannelList(state, hooks, windowId, m) {
1009
+ function handleChannelList(state, hooks, windowId, m, incomingDomain) {
922
1010
  const channels = [];
923
1011
  const set = state.channelsByWindow.get(windowId);
924
1012
  if (set) {
@@ -927,14 +1015,15 @@ function handleChannelList(state, hooks, windowId, m) {
927
1015
  if (peer) channels.push({ id: channelId, peer });
928
1016
  }
929
1017
  }
930
- 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 });
931
1019
  }
932
- function handleChannelClose(state, hooks, windowId, m) {
1020
+ function handleChannelClose(state, hooks, windowId, m, closerDomain) {
933
1021
  const channelId = m.channelId ?? "";
934
1022
  const peer = peerOf(state, channelId, windowId);
935
1023
  if (!peer) return;
936
- hooks.sendToNapplet(windowId, { type: "ifc.channel.closed", channelId });
937
- 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 });
938
1027
  removeChannel(state, channelId);
939
1028
  }
940
1029
  function removeWindowSubscriptions(state, windowId) {
@@ -948,7 +1037,11 @@ function removeWindowChannels(state, hooks, windowId) {
948
1037
  if (!channelIds) return;
949
1038
  for (const channelId of Array.from(channelIds)) {
950
1039
  const peer = peerOf(state, channelId, windowId);
951
- 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
+ }
952
1045
  removeChannel(state, channelId);
953
1046
  }
954
1047
  }
@@ -1041,7 +1134,7 @@ function handleStorageNub(windowId, msg, sendToNapplet, sessionRegistry, aclStat
1041
1134
  break;
1042
1135
  }
1043
1136
  case "clear": {
1044
- sendErrorNub("storage.clear is not in @napplet/nub/storage; action not supported");
1137
+ sendErrorNub("storage.clear is not in @napplet/nap/storage; action not supported");
1045
1138
  break;
1046
1139
  }
1047
1140
  case "keys": {
@@ -1080,7 +1173,8 @@ function createRuntimeDomainHandlers(context) {
1080
1173
  resource: (windowId, msg) => handleServiceOnlyMessage(context, "resource", windowId, msg),
1081
1174
  cvm: (windowId, msg) => handleServiceOnlyMessage(context, "cvm", windowId, msg),
1082
1175
  outbox: (windowId, msg) => handleServiceOnlyMessage(context, "outbox", windowId, msg),
1083
- 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)
1084
1178
  };
1085
1179
  }
1086
1180
  function handleStorageMessage(context, windowId, msg) {
@@ -1209,19 +1303,21 @@ function createNubEnvelopeDispatcher(handlers) {
1209
1303
  const adapt = (handler) => (msg) => {
1210
1304
  if (currentWindowId !== null) handler(currentWindowId, msg);
1211
1305
  };
1212
- nubDispatch.registerNub("relay", adapt(handlers.relay));
1213
- nubDispatch.registerNub("identity", adapt(handlers.identity));
1214
- nubDispatch.registerNub("keys", adapt(handlers.keys));
1215
- nubDispatch.registerNub("media", adapt(handlers.media));
1216
- nubDispatch.registerNub("notify", adapt(handlers.notify));
1217
- nubDispatch.registerNub("storage", adapt(handlers.storage));
1218
- nubDispatch.registerNub("ifc", adapt(handlers.ifc));
1219
- nubDispatch.registerNub("theme", adapt(handlers.theme));
1220
- nubDispatch.registerNub("config", adapt(handlers.config));
1221
- nubDispatch.registerNub("resource", adapt(handlers.resource));
1222
- nubDispatch.registerNub("cvm", adapt(handlers.cvm));
1223
- nubDispatch.registerNub("outbox", adapt(handlers.outbox));
1224
- nubDispatch.registerNub("upload", adapt(handlers.upload));
1306
+ nubDispatch.registerNap("relay", adapt(handlers.relay));
1307
+ nubDispatch.registerNap("identity", adapt(handlers.identity));
1308
+ nubDispatch.registerNap("keys", adapt(handlers.keys));
1309
+ nubDispatch.registerNap("media", adapt(handlers.media));
1310
+ nubDispatch.registerNap("notify", adapt(handlers.notify));
1311
+ nubDispatch.registerNap("storage", adapt(handlers.storage));
1312
+ nubDispatch.registerNap("ifc", adapt(handlers.ifc));
1313
+ nubDispatch.registerNap("inc", adapt(handlers.ifc));
1314
+ nubDispatch.registerNap("theme", adapt(handlers.theme));
1315
+ nubDispatch.registerNap("config", adapt(handlers.config));
1316
+ nubDispatch.registerNap("resource", adapt(handlers.resource));
1317
+ nubDispatch.registerNap("cvm", adapt(handlers.cvm));
1318
+ nubDispatch.registerNap("outbox", adapt(handlers.outbox));
1319
+ nubDispatch.registerNap("upload", adapt(handlers.upload));
1320
+ nubDispatch.registerNap("intent", adapt(handlers.intent));
1225
1321
  return (windowId, envelope) => {
1226
1322
  currentWindowId = windowId;
1227
1323
  try {
@@ -1231,7 +1327,67 @@ function createNubEnvelopeDispatcher(handlers) {
1231
1327
  }
1232
1328
  };
1233
1329
  }
1234
- 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) {
1235
1391
  return (windowId, msg) => {
1236
1392
  if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
1237
1393
  const envelope = msg;
@@ -1249,6 +1405,8 @@ function createMessageHandler(hooks, enforceNub, dispatchNubEnvelope) {
1249
1405
  return;
1250
1406
  }
1251
1407
  }
1408
+ const verdict = firewallGate(windowId, envelope, caps.senderCap);
1409
+ if (verdict === "drop") return;
1252
1410
  dispatchNubEnvelope(windowId, envelope);
1253
1411
  };
1254
1412
  }
@@ -1267,6 +1425,7 @@ function createInjectedEvent(hooks, topic, payload) {
1267
1425
  function createRuntimeInstance(context) {
1268
1426
  const {
1269
1427
  aclState,
1428
+ firewallState,
1270
1429
  eventBuffer,
1271
1430
  hooks,
1272
1431
  ifcRuntime,
@@ -1275,10 +1434,10 @@ function createRuntimeInstance(context) {
1275
1434
  replayDetector,
1276
1435
  serviceRegistry,
1277
1436
  sessionRegistry,
1278
- subscriptions
1437
+ subscriptions,
1438
+ consentHandlerRef
1279
1439
  } = context;
1280
1440
  const undeclaredServiceConsents = /* @__PURE__ */ new Set();
1281
- let consentHandler = null;
1282
1441
  return {
1283
1442
  handleMessage: context.handleMessage,
1284
1443
  injectEvent(topic, payload) {
@@ -1287,6 +1446,7 @@ function createRuntimeInstance(context) {
1287
1446
  destroy() {
1288
1447
  manifestCache.persist();
1289
1448
  aclState.persist();
1449
+ firewallState.persist();
1290
1450
  replayDetector.clear();
1291
1451
  subscriptions.clear();
1292
1452
  ifcRuntime.clear();
@@ -1295,8 +1455,7 @@ function createRuntimeInstance(context) {
1295
1455
  undeclaredServiceConsents.clear();
1296
1456
  },
1297
1457
  registerConsentHandler(handler) {
1298
- consentHandler = handler;
1299
- void consentHandler;
1458
+ consentHandlerRef.current = handler;
1300
1459
  },
1301
1460
  registerService(name, handler) {
1302
1461
  serviceRegistry[name] = handler;
@@ -1326,6 +1485,9 @@ function createRuntimeInstance(context) {
1326
1485
  get aclState() {
1327
1486
  return aclState;
1328
1487
  },
1488
+ get firewallState() {
1489
+ return firewallState;
1490
+ },
1329
1491
  get manifestCache() {
1330
1492
  return manifestCache;
1331
1493
  }
@@ -1337,10 +1499,26 @@ function createRuntime(hooks) {
1337
1499
  const registeredServices = createRegisteredServices(serviceRegistry);
1338
1500
  const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate);
1339
1501
  const aclState = createAclState(hooks.aclPersistence);
1502
+ const firewallState = createFirewallState(hooks.firewallPersistence);
1340
1503
  const manifestCache = createManifestCache(hooks.manifestPersistence);
1341
1504
  const replayDetector = createReplayDetector(
1342
1505
  hooks.getConfigOverrides ? () => hooks.getConfigOverrides().replayWindowSeconds : void 0
1343
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
+ };
1344
1522
  const enforce = createEnforceGate({
1345
1523
  checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
1346
1524
  resolveIdentity: (pubkey) => {
@@ -1357,6 +1535,7 @@ function createRuntime(hooks) {
1357
1535
  },
1358
1536
  onAclCheck: hooks.onAclCheck
1359
1537
  });
1538
+ const firewallGate = createFirewallGate({ firewallState, sessionRegistry, hooks, fireConsent });
1360
1539
  const eventBuffer = createEventBuffer(
1361
1540
  hooks.sendToNapplet,
1362
1541
  sessionRegistry,
@@ -1365,6 +1544,7 @@ function createRuntime(hooks) {
1365
1544
  hooks.getConfigOverrides ? () => hooks.getConfigOverrides().ringBufferSize ?? RING_BUFFER_SIZE : void 0
1366
1545
  );
1367
1546
  aclState.load();
1547
+ firewallState.load();
1368
1548
  manifestCache.load();
1369
1549
  const ifcRuntime = createIfcRuntime(hooks, sessionRegistry);
1370
1550
  const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry, aclState });
@@ -1374,7 +1554,7 @@ function createRuntime(hooks) {
1374
1554
  ifc: ifcRuntime.handleMessage,
1375
1555
  ...domainHandlers
1376
1556
  });
1377
- const handleMessage = createMessageHandler(hooks, enforceNub, dispatchNubEnvelope);
1557
+ const handleMessage = createMessageHandler(hooks, enforceNub, dispatchNubEnvelope, firewallGate);
1378
1558
  return createRuntimeInstance({
1379
1559
  hooks,
1380
1560
  serviceRegistry,
@@ -1385,7 +1565,9 @@ function createRuntime(hooks) {
1385
1565
  ifcRuntime,
1386
1566
  sessionRegistry,
1387
1567
  aclState,
1568
+ firewallState,
1388
1569
  manifestCache,
1570
+ consentHandlerRef,
1389
1571
  handleMessage
1390
1572
  });
1391
1573
  }
@@ -1399,6 +1581,7 @@ export {
1399
1581
  createAclState,
1400
1582
  createEnforceGate,
1401
1583
  createEventBuffer,
1584
+ createFirewallState,
1402
1585
  createManifestCache,
1403
1586
  createNappKeyRegistry,
1404
1587
  createNubEnforceGate,