@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.d.ts +286 -83
- package/dist/index.js +219 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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 !== "
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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)
|
|
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)
|
|
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:
|
|
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:
|
|
939
|
-
|
|
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)
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|