@kehto/runtime 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -20
- package/dist/index.d.ts +49 -58
- package/dist/index.js +802 -879
- package/dist/index.js.map +1 -1
- package/package.json +8 -22
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// src/enforce.ts
|
|
2
|
+
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
2
3
|
import { resolveCapabilitiesNub } from "@kehto/acl";
|
|
4
|
+
var CLASS_CAPABILITY_ALLOWLIST = Object.freeze({
|
|
5
|
+
"class-1": new Set(ALL_CAPABILITIES),
|
|
6
|
+
"class-2": new Set(ALL_CAPABILITIES.filter(
|
|
7
|
+
(c) => c !== "relay:write" && c !== "identity:decrypt" && c !== "outbox:write"
|
|
8
|
+
))
|
|
9
|
+
});
|
|
3
10
|
function createEnforceGate(config) {
|
|
4
11
|
const { checkAcl, resolveIdentity, onAclCheck } = config;
|
|
5
12
|
return function enforce(pubkey, capability, message) {
|
|
@@ -9,10 +16,11 @@ function createEnforceGate(config) {
|
|
|
9
16
|
const allowed = checkAcl(pubkey, dTag, aggregateHash, capability);
|
|
10
17
|
const identity = { pubkey, dTag, hash: aggregateHash };
|
|
11
18
|
const decision = allowed ? "allow" : "deny";
|
|
19
|
+
const reason = allowed ? "allowed" : "capability-missing";
|
|
12
20
|
if (onAclCheck) {
|
|
13
|
-
onAclCheck({ identity, capability, decision, message });
|
|
21
|
+
onAclCheck({ identity, capability, decision, message, reason });
|
|
14
22
|
}
|
|
15
|
-
return { allowed, capability };
|
|
23
|
+
return { allowed, capability, reason };
|
|
16
24
|
};
|
|
17
25
|
}
|
|
18
26
|
function createNubEnforceGate(config) {
|
|
@@ -21,13 +29,31 @@ function createNubEnforceGate(config) {
|
|
|
21
29
|
const entry = resolveIdentityByWindowId(windowId);
|
|
22
30
|
const dTag = entry?.dTag ?? "";
|
|
23
31
|
const aggregateHash = entry?.aggregateHash ?? "";
|
|
32
|
+
const nappletClass = entry?.class ?? null;
|
|
33
|
+
if (nappletClass !== null) {
|
|
34
|
+
const allowlist = CLASS_CAPABILITY_ALLOWLIST[nappletClass];
|
|
35
|
+
if (!allowlist || !allowlist.has(capability)) {
|
|
36
|
+
const identity2 = { pubkey: "", dTag, hash: aggregateHash };
|
|
37
|
+
if (onAclCheck) {
|
|
38
|
+
onAclCheck({
|
|
39
|
+
identity: identity2,
|
|
40
|
+
capability,
|
|
41
|
+
decision: "deny",
|
|
42
|
+
message,
|
|
43
|
+
reason: "class-forbidden"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { allowed: false, capability, reason: "class-forbidden" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
24
49
|
const allowed = checkAcl("", dTag, aggregateHash, capability);
|
|
25
50
|
const identity = { pubkey: "", dTag, hash: aggregateHash };
|
|
26
51
|
const decision = allowed ? "allow" : "deny";
|
|
52
|
+
const reason = allowed ? "allowed" : "capability-missing";
|
|
27
53
|
if (onAclCheck) {
|
|
28
|
-
onAclCheck({ identity, capability, decision, message });
|
|
54
|
+
onAclCheck({ identity, capability, decision, message, reason });
|
|
29
55
|
}
|
|
30
|
-
return { allowed, capability };
|
|
56
|
+
return { allowed, capability, reason };
|
|
31
57
|
};
|
|
32
58
|
}
|
|
33
59
|
function formatDenialReason(capability) {
|
|
@@ -117,7 +143,7 @@ import {
|
|
|
117
143
|
CAP_HOTKEY_FORWARD,
|
|
118
144
|
CAP_STATE_READ,
|
|
119
145
|
CAP_STATE_WRITE,
|
|
120
|
-
|
|
146
|
+
toKey
|
|
121
147
|
} from "@kehto/acl";
|
|
122
148
|
var CAP_IDENTITY_READ = 1 << 5;
|
|
123
149
|
var CAP_KEYS_BIND = 1 << 6;
|
|
@@ -126,6 +152,12 @@ var CAP_MEDIA_CONTROL = 1 << 10;
|
|
|
126
152
|
var CAP_NOTIFY_SEND = 1 << 11;
|
|
127
153
|
var CAP_NOTIFY_CHANNEL = 1 << 12;
|
|
128
154
|
var CAP_THEME_READ = 1 << 13;
|
|
155
|
+
var CAP_CONFIG_READ = 1 << 14;
|
|
156
|
+
var CAP_RESOURCE_FETCH = 1 << 15;
|
|
157
|
+
var CAP_IDENTITY_DECRYPT = 1 << 16;
|
|
158
|
+
var CAP_CVM_CALL = 1 << 17;
|
|
159
|
+
var CAP_OUTBOX_READ = 1 << 18;
|
|
160
|
+
var CAP_OUTBOX_WRITE = 1 << 19;
|
|
129
161
|
var CAP_MAP = {
|
|
130
162
|
"relay:read": CAP_RELAY_READ,
|
|
131
163
|
"relay:write": CAP_RELAY_WRITE,
|
|
@@ -140,8 +172,15 @@ var CAP_MAP = {
|
|
|
140
172
|
"media:control": CAP_MEDIA_CONTROL,
|
|
141
173
|
"notify:send": CAP_NOTIFY_SEND,
|
|
142
174
|
"notify:channel": CAP_NOTIFY_CHANNEL,
|
|
143
|
-
"theme:read": CAP_THEME_READ
|
|
175
|
+
"theme:read": CAP_THEME_READ,
|
|
176
|
+
"config:read": CAP_CONFIG_READ,
|
|
177
|
+
"resource:fetch": CAP_RESOURCE_FETCH,
|
|
178
|
+
"identity:decrypt": CAP_IDENTITY_DECRYPT,
|
|
179
|
+
"cvm:call": CAP_CVM_CALL,
|
|
180
|
+
"outbox:read": CAP_OUTBOX_READ,
|
|
181
|
+
"outbox:write": CAP_OUTBOX_WRITE
|
|
144
182
|
};
|
|
183
|
+
var RUNTIME_CAP_ALL = Object.values(CAP_MAP).reduce((bits, bit) => bits | bit, 0);
|
|
145
184
|
function capToBit(cap) {
|
|
146
185
|
return CAP_MAP[cap] ?? 0;
|
|
147
186
|
}
|
|
@@ -157,6 +196,11 @@ function toIdentity(pubkey, dTag, hash) {
|
|
|
157
196
|
}
|
|
158
197
|
function createAclState(persistence, defaultPolicy = "permissive") {
|
|
159
198
|
let state = createState(defaultPolicy);
|
|
199
|
+
function ensureRuntimeDefaultEntry(id) {
|
|
200
|
+
if (state.defaultPolicy !== "permissive") return;
|
|
201
|
+
if (state.entries[toKey(id)]) return;
|
|
202
|
+
state = grant(state, id, RUNTIME_CAP_ALL);
|
|
203
|
+
}
|
|
160
204
|
return {
|
|
161
205
|
check(pubkey, dTag, aggregateHash, capability) {
|
|
162
206
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
@@ -164,23 +208,27 @@ function createAclState(persistence, defaultPolicy = "permissive") {
|
|
|
164
208
|
},
|
|
165
209
|
grant(pubkey, dTag, aggregateHash, capability) {
|
|
166
210
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
211
|
+
ensureRuntimeDefaultEntry(id);
|
|
167
212
|
state = grant(state, id, capToBit(capability));
|
|
168
213
|
},
|
|
169
214
|
revoke(pubkey, dTag, aggregateHash, capability) {
|
|
170
215
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
216
|
+
ensureRuntimeDefaultEntry(id);
|
|
171
217
|
state = revoke(state, id, capToBit(capability));
|
|
172
218
|
},
|
|
173
219
|
block(pubkey, dTag, aggregateHash) {
|
|
174
220
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
221
|
+
ensureRuntimeDefaultEntry(id);
|
|
175
222
|
state = block(state, id);
|
|
176
223
|
},
|
|
177
224
|
unblock(pubkey, dTag, aggregateHash) {
|
|
178
225
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
226
|
+
ensureRuntimeDefaultEntry(id);
|
|
179
227
|
state = unblock(state, id);
|
|
180
228
|
},
|
|
181
229
|
isBlocked(pubkey, dTag, aggregateHash) {
|
|
182
230
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
183
|
-
return !check(state, id,
|
|
231
|
+
return !check(state, id, RUNTIME_CAP_ALL) && this.getEntry(pubkey, dTag, aggregateHash)?.blocked === true;
|
|
184
232
|
},
|
|
185
233
|
getEntry(pubkey, dTag, aggregateHash) {
|
|
186
234
|
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
@@ -195,8 +243,7 @@ function createAclState(persistence, defaultPolicy = "permissive") {
|
|
|
195
243
|
};
|
|
196
244
|
},
|
|
197
245
|
getAllEntries() {
|
|
198
|
-
return Object.entries(state.entries).map(([
|
|
199
|
-
const parts = key.split(":");
|
|
246
|
+
return Object.entries(state.entries).map(([, entry]) => {
|
|
200
247
|
return {
|
|
201
248
|
pubkey: "",
|
|
202
249
|
capabilities: bitsToCapabilities(entry.caps),
|
|
@@ -391,22 +438,21 @@ function createEventBuffer(sendToNapplet, sessionRegistry, enforce, subscription
|
|
|
391
438
|
|
|
392
439
|
// src/runtime.ts
|
|
393
440
|
import { createDispatch } from "@napplet/core";
|
|
394
|
-
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
395
441
|
|
|
396
442
|
// src/service-dispatch.ts
|
|
397
443
|
function routeServiceMessage(windowId, message, services, sendToNapplet) {
|
|
398
|
-
const send = (msg) => sendToNapplet(windowId, msg);
|
|
399
444
|
const domain = message.type.split(".")[0];
|
|
400
445
|
const handler = services[domain];
|
|
401
446
|
if (handler) {
|
|
402
|
-
handler.handleMessage(windowId, message,
|
|
447
|
+
handler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
403
448
|
return true;
|
|
404
449
|
}
|
|
405
|
-
|
|
406
|
-
|
|
450
|
+
const ifcMessage = message;
|
|
451
|
+
if (message.type === "ifc.emit" && typeof ifcMessage.topic === "string") {
|
|
452
|
+
const prefix = ifcMessage.topic.split(":")[0];
|
|
407
453
|
const ifcHandler = services[prefix];
|
|
408
454
|
if (ifcHandler) {
|
|
409
|
-
ifcHandler.handleMessage(windowId, message,
|
|
455
|
+
ifcHandler.handleMessage(windowId, message, (msg) => sendToNapplet(windowId, msg));
|
|
410
456
|
return true;
|
|
411
457
|
}
|
|
412
458
|
}
|
|
@@ -421,6 +467,492 @@ function notifyServiceWindowDestroyed(windowId, services) {
|
|
|
421
467
|
}
|
|
422
468
|
}
|
|
423
469
|
|
|
470
|
+
// src/relay-handler.ts
|
|
471
|
+
function createRelayHandler(context) {
|
|
472
|
+
return function handleRelayMessage(windowId, msg) {
|
|
473
|
+
const m = msg;
|
|
474
|
+
const dotIdx = msg.type.indexOf(".");
|
|
475
|
+
const action = msg.type.slice(dotIdx + 1);
|
|
476
|
+
switch (action) {
|
|
477
|
+
case "subscribe":
|
|
478
|
+
handleRelaySubscribe(context, windowId, msg, m);
|
|
479
|
+
return;
|
|
480
|
+
case "close":
|
|
481
|
+
handleRelayClose(context, windowId, msg, m);
|
|
482
|
+
return;
|
|
483
|
+
case "publish":
|
|
484
|
+
handleRelayPublish(context, windowId, msg, m);
|
|
485
|
+
return;
|
|
486
|
+
case "publishEncrypted":
|
|
487
|
+
handleRelayPublishEncrypted(context, windowId, msg);
|
|
488
|
+
return;
|
|
489
|
+
case "query":
|
|
490
|
+
handleRelayQuery(context, windowId, m);
|
|
491
|
+
return;
|
|
492
|
+
default:
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function relayServiceFrom(context) {
|
|
498
|
+
return context.serviceRegistry["relay"] ?? context.serviceRegistry["relay-pool"];
|
|
499
|
+
}
|
|
500
|
+
function isShellKindQuery(filters) {
|
|
501
|
+
return filters.length > 0 && filters.every((filter) => filter.kinds?.every((kind) => kind >= 29e3 && kind < 3e4));
|
|
502
|
+
}
|
|
503
|
+
function handleRelaySubscribe(context, windowId, msg, m) {
|
|
504
|
+
const { eventBuffer, hooks, serviceRegistry, subscriptions } = context;
|
|
505
|
+
const subId = m.subId ?? "";
|
|
506
|
+
const filters = m.filters ?? [];
|
|
507
|
+
if (!subId) return;
|
|
508
|
+
const subKey = `${windowId}:${subId}`;
|
|
509
|
+
subscriptions.set(subKey, { windowId, filters });
|
|
510
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
511
|
+
function deliver(event) {
|
|
512
|
+
if (seenIds.has(event.id)) return;
|
|
513
|
+
seenIds.add(event.id);
|
|
514
|
+
if (subscriptions.has(subKey)) {
|
|
515
|
+
hooks.sendToNapplet(windowId, { type: "relay.event", subId, event });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const bufferedEvent of eventBuffer.getBufferedEvents()) {
|
|
519
|
+
if (matchesAnyFilter(bufferedEvent, filters)) deliver(bufferedEvent);
|
|
520
|
+
}
|
|
521
|
+
const isShellKind = isShellKindQuery(filters);
|
|
522
|
+
const relayService = relayServiceFrom(context);
|
|
523
|
+
const cacheService = !serviceRegistry["relay"] ? serviceRegistry["cache"] : void 0;
|
|
524
|
+
if (!isShellKind && relayService) {
|
|
525
|
+
relayService.handleMessage(windowId, msg, (resp) => {
|
|
526
|
+
if (!subscriptions.has(subKey)) return;
|
|
527
|
+
hooks.sendToNapplet(windowId, resp);
|
|
528
|
+
});
|
|
529
|
+
if (cacheService) {
|
|
530
|
+
cacheService.handleMessage(windowId, msg, (resp) => {
|
|
531
|
+
if (!subscriptions.has(subKey)) return;
|
|
532
|
+
hooks.sendToNapplet(windowId, resp);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, isShellKind, deliver);
|
|
538
|
+
}
|
|
539
|
+
function deliverFromRuntimeBackends(context, windowId, subId, subKey, filters, isShellKind, deliver) {
|
|
540
|
+
const { hooks } = context;
|
|
541
|
+
const cache = hooks.cache;
|
|
542
|
+
if (cache?.isAvailable() && !isShellKind) {
|
|
543
|
+
cache.query(filters).then((cachedEvents) => {
|
|
544
|
+
for (const event of cachedEvents) deliver(event);
|
|
545
|
+
}).catch(() => {
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const pool = hooks.relayPool;
|
|
549
|
+
if (!pool?.isAvailable() && !isShellKind) {
|
|
550
|
+
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (!pool?.isAvailable() || isShellKind) return;
|
|
554
|
+
const relayUrls = pool.selectRelayTier(filters);
|
|
555
|
+
let eoseSent = false;
|
|
556
|
+
const eoseFallbackTimer = setTimeout(() => {
|
|
557
|
+
if (!eoseSent) {
|
|
558
|
+
eoseSent = true;
|
|
559
|
+
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
560
|
+
}
|
|
561
|
+
}, 15e3);
|
|
562
|
+
const subscription = pool.subscribe(filters, (item) => {
|
|
563
|
+
if (item === "EOSE") {
|
|
564
|
+
clearTimeout(eoseFallbackTimer);
|
|
565
|
+
if (!eoseSent) {
|
|
566
|
+
eoseSent = true;
|
|
567
|
+
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
deliver(item);
|
|
572
|
+
if (cache?.isAvailable() && !isShellKind) {
|
|
573
|
+
try {
|
|
574
|
+
cache.store(item);
|
|
575
|
+
} catch {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}, relayUrls);
|
|
580
|
+
pool.trackSubscription(subKey, () => {
|
|
581
|
+
clearTimeout(eoseFallbackTimer);
|
|
582
|
+
subscription.unsubscribe();
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
function handleRelayClose(context, windowId, msg, m) {
|
|
586
|
+
const { hooks, subscriptions } = context;
|
|
587
|
+
const subId = m.subId ?? "";
|
|
588
|
+
if (!subId) return;
|
|
589
|
+
const subKey = `${windowId}:${subId}`;
|
|
590
|
+
subscriptions.delete(subKey);
|
|
591
|
+
const relayService = relayServiceFrom(context);
|
|
592
|
+
if (relayService) relayService.handleMessage(windowId, msg, () => {
|
|
593
|
+
});
|
|
594
|
+
hooks.relayPool?.untrackSubscription(subKey);
|
|
595
|
+
hooks.sendToNapplet(windowId, { type: "relay.closed", subId, message: "" });
|
|
596
|
+
}
|
|
597
|
+
function handleRelayPublish(context, windowId, msg, m) {
|
|
598
|
+
const { eventBuffer, hooks, replayDetector } = context;
|
|
599
|
+
const event = m.event;
|
|
600
|
+
const id = m.id ?? "";
|
|
601
|
+
if (!event || typeof event !== "object") {
|
|
602
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.error", id, error: "invalid event" });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const replayResult = replayDetector.check(event);
|
|
606
|
+
if (replayResult !== null) {
|
|
607
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: replayResult });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const relayService = relayServiceFrom(context);
|
|
611
|
+
if (relayService) {
|
|
612
|
+
relayService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
613
|
+
} else if (hooks.relayPool?.isAvailable()) {
|
|
614
|
+
hooks.relayPool.publish(event);
|
|
615
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: true });
|
|
616
|
+
} else {
|
|
617
|
+
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: "no relay pool available" });
|
|
618
|
+
}
|
|
619
|
+
eventBuffer.bufferAndDeliver(event, windowId);
|
|
620
|
+
}
|
|
621
|
+
function handleRelayPublishEncrypted(context, windowId, msg) {
|
|
622
|
+
const { hooks } = context;
|
|
623
|
+
const id = msg.id ?? "";
|
|
624
|
+
const eventTemplate = msg.event;
|
|
625
|
+
const peMsg = msg;
|
|
626
|
+
const recipient = peMsg.recipient ?? "";
|
|
627
|
+
const encryption = peMsg.encryption ?? "nip44";
|
|
628
|
+
const replyPe = (ok, extra = {}) => {
|
|
629
|
+
hooks.sendToNapplet(windowId, { type: "relay.publishEncrypted.result", id, ok, ...extra });
|
|
630
|
+
};
|
|
631
|
+
if (!recipient) {
|
|
632
|
+
replyPe(false, { error: "missing recipient" });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (encryption !== "nip44" && encryption !== "nip04") {
|
|
636
|
+
replyPe(false, { error: `unsupported encryption scheme: ${encryption}` });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const peSigner = hooks.auth.getSigner();
|
|
640
|
+
if (!peSigner) {
|
|
641
|
+
replyPe(false, { error: "no signer configured" });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (!eventTemplate || typeof eventTemplate !== "object") {
|
|
645
|
+
replyPe(false, { error: "invalid event template" });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe);
|
|
649
|
+
}
|
|
650
|
+
function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe) {
|
|
651
|
+
const { eventBuffer, hooks } = context;
|
|
652
|
+
const peSigner = hooks.auth.getSigner();
|
|
653
|
+
if (!peSigner) return;
|
|
654
|
+
(async () => {
|
|
655
|
+
try {
|
|
656
|
+
const plaintext = String(eventTemplate.content ?? "");
|
|
657
|
+
const ciphertext = encryption === "nip44" ? await peSigner.nip44?.encrypt(recipient, plaintext) ?? "" : await peSigner.nip04?.encrypt(recipient, plaintext) ?? "";
|
|
658
|
+
const eventWithCiphertext = { ...eventTemplate, content: ciphertext };
|
|
659
|
+
const signed = await peSigner.signEvent?.(eventWithCiphertext);
|
|
660
|
+
if (!signed) {
|
|
661
|
+
replyPe(false, { error: "signEvent returned null" });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
publishSignedEncrypted(context, windowId, id, signed, replyPe);
|
|
665
|
+
try {
|
|
666
|
+
eventBuffer.bufferAndDeliver(signed, windowId);
|
|
667
|
+
} catch {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
} catch (err) {
|
|
671
|
+
replyPe(false, { error: err?.message ?? "encryption failed" });
|
|
672
|
+
}
|
|
673
|
+
})();
|
|
674
|
+
}
|
|
675
|
+
function publishSignedEncrypted(context, windowId, id, signed, replyPe) {
|
|
676
|
+
const { hooks } = context;
|
|
677
|
+
const relayService = relayServiceFrom(context);
|
|
678
|
+
if (!relayService) {
|
|
679
|
+
if (hooks.relayPool?.isAvailable()) {
|
|
680
|
+
hooks.relayPool.publish(signed);
|
|
681
|
+
replyPe(true, { event: signed, eventId: signed.id });
|
|
682
|
+
} else {
|
|
683
|
+
replyPe(false, { error: "no relay pool available" });
|
|
684
|
+
}
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const publishMsg = { type: "relay.publish", id, event: signed };
|
|
688
|
+
let replied = false;
|
|
689
|
+
relayService.handleMessage(windowId, publishMsg, (resp) => {
|
|
690
|
+
if (replied) return;
|
|
691
|
+
const r = resp;
|
|
692
|
+
if (typeof r.type !== "string" || !r.type.startsWith("relay.publish")) return;
|
|
693
|
+
const okVal = r.ok ?? r.accepted ?? false;
|
|
694
|
+
replied = true;
|
|
695
|
+
const publishResult = { event: signed, eventId: signed.id };
|
|
696
|
+
if (!okVal) publishResult.error = r.error ?? r.message ?? "publish failed";
|
|
697
|
+
replyPe(okVal, publishResult);
|
|
698
|
+
});
|
|
699
|
+
if (!replied) {
|
|
700
|
+
replied = true;
|
|
701
|
+
replyPe(true, { event: signed, eventId: signed.id });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function handleRelayQuery(context, windowId, m) {
|
|
705
|
+
const id = m.id ?? "";
|
|
706
|
+
const filters = m.filters ?? [];
|
|
707
|
+
let count = 0;
|
|
708
|
+
for (const event of context.eventBuffer.getBufferedEvents()) {
|
|
709
|
+
if (matchesAnyFilter(event, filters)) count++;
|
|
710
|
+
}
|
|
711
|
+
context.hooks.sendToNapplet(windowId, { type: "relay.query.result", id, count });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/identity-handler.ts
|
|
715
|
+
function createIdentityHandler(context) {
|
|
716
|
+
return function handleIdentityMessage(windowId, msg) {
|
|
717
|
+
const { hooks, serviceRegistry } = context;
|
|
718
|
+
const identityService = serviceRegistry["identity"];
|
|
719
|
+
if (identityService) {
|
|
720
|
+
identityService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const id = msg.id ?? "";
|
|
724
|
+
const action = msg.type.slice("identity.".length);
|
|
725
|
+
const signer = hooks.auth.getSigner();
|
|
726
|
+
const sendError = (error) => {
|
|
727
|
+
hooks.sendToNapplet(windowId, { type: `${msg.type}.error`, id, error });
|
|
728
|
+
};
|
|
729
|
+
const sendResult = (payload) => {
|
|
730
|
+
hooks.sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
731
|
+
};
|
|
732
|
+
switch (action) {
|
|
733
|
+
case "getPublicKey":
|
|
734
|
+
if (!signer) {
|
|
735
|
+
sendResult({ pubkey: "" });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey: pubkey ?? "" })).catch((err) => sendError(err?.message ?? "getPublicKey failed"));
|
|
739
|
+
return;
|
|
740
|
+
case "getRelays":
|
|
741
|
+
if (!signer) {
|
|
742
|
+
sendError("no signer configured");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch((err) => sendError(err?.message ?? "getRelays failed"));
|
|
746
|
+
return;
|
|
747
|
+
case "getProfile":
|
|
748
|
+
sendResult({ profile: null });
|
|
749
|
+
return;
|
|
750
|
+
case "getFollows":
|
|
751
|
+
sendResult({ pubkeys: [] });
|
|
752
|
+
return;
|
|
753
|
+
case "getList":
|
|
754
|
+
sendResult({ entries: [] });
|
|
755
|
+
return;
|
|
756
|
+
case "getZaps":
|
|
757
|
+
sendResult({ zaps: [] });
|
|
758
|
+
return;
|
|
759
|
+
case "getMutes":
|
|
760
|
+
sendResult({ pubkeys: [] });
|
|
761
|
+
return;
|
|
762
|
+
case "getBlocked":
|
|
763
|
+
sendResult({ pubkeys: [] });
|
|
764
|
+
return;
|
|
765
|
+
case "getBadges":
|
|
766
|
+
sendResult({ badges: [] });
|
|
767
|
+
return;
|
|
768
|
+
default:
|
|
769
|
+
sendError(`Unknown identity action: ${action}`);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/ifc-handler.ts
|
|
775
|
+
function createIfcRuntime(hooks, sessionRegistry) {
|
|
776
|
+
const state = {
|
|
777
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
778
|
+
channels: /* @__PURE__ */ new Map(),
|
|
779
|
+
channelsByWindow: /* @__PURE__ */ new Map()
|
|
780
|
+
};
|
|
781
|
+
return {
|
|
782
|
+
handleMessage(windowId, msg) {
|
|
783
|
+
handleIfcMessage(state, hooks, sessionRegistry, windowId, msg);
|
|
784
|
+
},
|
|
785
|
+
destroyWindow(windowId) {
|
|
786
|
+
removeWindowChannels(state, hooks, windowId);
|
|
787
|
+
removeWindowSubscriptions(state, windowId);
|
|
788
|
+
},
|
|
789
|
+
clear() {
|
|
790
|
+
state.subscriptions.clear();
|
|
791
|
+
state.channels.clear();
|
|
792
|
+
state.channelsByWindow.clear();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function addChannel(state, channelId, peerA, peerB) {
|
|
797
|
+
state.channels.set(channelId, { channelId, peerA, peerB });
|
|
798
|
+
for (const windowId of [peerA, peerB]) {
|
|
799
|
+
let set = state.channelsByWindow.get(windowId);
|
|
800
|
+
if (!set) {
|
|
801
|
+
set = /* @__PURE__ */ new Set();
|
|
802
|
+
state.channelsByWindow.set(windowId, set);
|
|
803
|
+
}
|
|
804
|
+
set.add(channelId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function removeChannel(state, channelId) {
|
|
808
|
+
const channel = state.channels.get(channelId);
|
|
809
|
+
if (!channel) return;
|
|
810
|
+
state.channels.delete(channelId);
|
|
811
|
+
for (const windowId of [channel.peerA, channel.peerB]) {
|
|
812
|
+
const set = state.channelsByWindow.get(windowId);
|
|
813
|
+
if (!set) continue;
|
|
814
|
+
set.delete(channelId);
|
|
815
|
+
if (set.size === 0) state.channelsByWindow.delete(windowId);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function peerOf(state, channelId, self) {
|
|
819
|
+
const channel = state.channels.get(channelId);
|
|
820
|
+
if (!channel) return null;
|
|
821
|
+
if (channel.peerA === self) return channel.peerB;
|
|
822
|
+
if (channel.peerB === self) return channel.peerA;
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
function resolveTarget(sessionRegistry, target) {
|
|
826
|
+
if (sessionRegistry.getEntryByWindowId(target)) return target;
|
|
827
|
+
const entries = sessionRegistry.getAllEntries();
|
|
828
|
+
const byPubkey = entries.find((entry) => entry.pubkey === target);
|
|
829
|
+
return byPubkey?.windowId ?? null;
|
|
830
|
+
}
|
|
831
|
+
function handleIfcMessage(state, hooks, sessionRegistry, windowId, msg) {
|
|
832
|
+
const m = msg;
|
|
833
|
+
const dotIdx = msg.type.indexOf(".");
|
|
834
|
+
const action = msg.type.slice(dotIdx + 1);
|
|
835
|
+
switch (action) {
|
|
836
|
+
case "emit":
|
|
837
|
+
handleEmit(state, hooks, windowId, m);
|
|
838
|
+
return;
|
|
839
|
+
case "subscribe":
|
|
840
|
+
handleSubscribe(state, hooks, windowId, m);
|
|
841
|
+
return;
|
|
842
|
+
case "unsubscribe":
|
|
843
|
+
handleUnsubscribe(state, windowId, m);
|
|
844
|
+
return;
|
|
845
|
+
case "channel.open":
|
|
846
|
+
handleChannelOpen(state, hooks, sessionRegistry, windowId, m);
|
|
847
|
+
return;
|
|
848
|
+
case "channel.emit":
|
|
849
|
+
handleChannelEmit(state, hooks, windowId, m);
|
|
850
|
+
return;
|
|
851
|
+
case "channel.broadcast":
|
|
852
|
+
handleChannelBroadcast(state, hooks, windowId, m);
|
|
853
|
+
return;
|
|
854
|
+
case "channel.list":
|
|
855
|
+
handleChannelList(state, hooks, windowId, m);
|
|
856
|
+
return;
|
|
857
|
+
case "channel.close":
|
|
858
|
+
handleChannelClose(state, hooks, windowId, m);
|
|
859
|
+
return;
|
|
860
|
+
default:
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function handleEmit(state, hooks, windowId, m) {
|
|
865
|
+
const topic = m.topic ?? "";
|
|
866
|
+
if (!topic) return;
|
|
867
|
+
const subscribers = state.subscriptions.get(topic);
|
|
868
|
+
if (!subscribers) return;
|
|
869
|
+
for (const subscriberWindowId of subscribers) {
|
|
870
|
+
if (subscriberWindowId !== windowId) {
|
|
871
|
+
hooks.sendToNapplet(subscriberWindowId, { type: "ifc.event", topic, payload: m.payload, sender: windowId });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function handleSubscribe(state, hooks, windowId, m) {
|
|
876
|
+
const id = m.id ?? "";
|
|
877
|
+
const topic = m.topic ?? "";
|
|
878
|
+
if (!topic) {
|
|
879
|
+
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id, error: "missing topic" });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
let subscriptions = state.subscriptions.get(topic);
|
|
883
|
+
if (!subscriptions) {
|
|
884
|
+
subscriptions = /* @__PURE__ */ new Set();
|
|
885
|
+
state.subscriptions.set(topic, subscriptions);
|
|
886
|
+
}
|
|
887
|
+
subscriptions.add(windowId);
|
|
888
|
+
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id });
|
|
889
|
+
}
|
|
890
|
+
function handleUnsubscribe(state, windowId, m) {
|
|
891
|
+
const topic = m.topic ?? "";
|
|
892
|
+
if (!topic) return;
|
|
893
|
+
const subscriptions = state.subscriptions.get(topic);
|
|
894
|
+
if (!subscriptions) return;
|
|
895
|
+
subscriptions.delete(windowId);
|
|
896
|
+
if (subscriptions.size === 0) state.subscriptions.delete(topic);
|
|
897
|
+
}
|
|
898
|
+
function handleChannelOpen(state, hooks, sessionRegistry, windowId, m) {
|
|
899
|
+
const id = m.id ?? "";
|
|
900
|
+
const peerWindow = resolveTarget(sessionRegistry, m.target ?? "");
|
|
901
|
+
if (!peerWindow) {
|
|
902
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, error: "target not found" });
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const channelId = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
|
|
906
|
+
addChannel(state, channelId, windowId, peerWindow);
|
|
907
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, channelId, peer: peerWindow });
|
|
908
|
+
}
|
|
909
|
+
function handleChannelEmit(state, hooks, windowId, m) {
|
|
910
|
+
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 });
|
|
912
|
+
}
|
|
913
|
+
function handleChannelBroadcast(state, hooks, windowId, m) {
|
|
914
|
+
const channels = state.channelsByWindow.get(windowId);
|
|
915
|
+
if (!channels) return;
|
|
916
|
+
for (const channelId of channels) {
|
|
917
|
+
const peer = peerOf(state, channelId, windowId);
|
|
918
|
+
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function handleChannelList(state, hooks, windowId, m) {
|
|
922
|
+
const channels = [];
|
|
923
|
+
const set = state.channelsByWindow.get(windowId);
|
|
924
|
+
if (set) {
|
|
925
|
+
for (const channelId of set) {
|
|
926
|
+
const peer = peerOf(state, channelId, windowId);
|
|
927
|
+
if (peer) channels.push({ id: channelId, peer });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.list.result", id: m.id ?? "", channels });
|
|
931
|
+
}
|
|
932
|
+
function handleChannelClose(state, hooks, windowId, m) {
|
|
933
|
+
const channelId = m.channelId ?? "";
|
|
934
|
+
const peer = peerOf(state, channelId, windowId);
|
|
935
|
+
if (!peer) return;
|
|
936
|
+
hooks.sendToNapplet(windowId, { type: "ifc.channel.closed", channelId });
|
|
937
|
+
hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
938
|
+
removeChannel(state, channelId);
|
|
939
|
+
}
|
|
940
|
+
function removeWindowSubscriptions(state, windowId) {
|
|
941
|
+
for (const [topic, subscriptions] of state.subscriptions) {
|
|
942
|
+
subscriptions.delete(windowId);
|
|
943
|
+
if (subscriptions.size === 0) state.subscriptions.delete(topic);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function removeWindowChannels(state, hooks, windowId) {
|
|
947
|
+
const channelIds = state.channelsByWindow.get(windowId);
|
|
948
|
+
if (!channelIds) return;
|
|
949
|
+
for (const channelId of Array.from(channelIds)) {
|
|
950
|
+
const peer = peerOf(state, channelId, windowId);
|
|
951
|
+
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
952
|
+
removeChannel(state, channelId);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
424
956
|
// src/state-handler.ts
|
|
425
957
|
function scopedKey(dTag, aggregateHash, userKey) {
|
|
426
958
|
return `napplet-state:${dTag}:${aggregateHash}:${userKey}`;
|
|
@@ -450,7 +982,7 @@ function handleStorageNub(windowId, msg, sendToNapplet, sessionRegistry, aclStat
|
|
|
450
982
|
sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
451
983
|
}
|
|
452
984
|
function sendErrorNub(error) {
|
|
453
|
-
sendToNapplet(windowId, { type: `${msg.type}.
|
|
985
|
+
sendToNapplet(windowId, { type: `${msg.type}.result`, id, error });
|
|
454
986
|
}
|
|
455
987
|
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
456
988
|
if (!entry) {
|
|
@@ -509,7 +1041,7 @@ function handleStorageNub(windowId, msg, sendToNapplet, sessionRegistry, aclStat
|
|
|
509
1041
|
break;
|
|
510
1042
|
}
|
|
511
1043
|
case "clear": {
|
|
512
|
-
sendErrorNub("storage.clear is not in @napplet/nub
|
|
1044
|
+
sendErrorNub("storage.clear is not in @napplet/nub/storage; action not supported");
|
|
513
1045
|
break;
|
|
514
1046
|
}
|
|
515
1047
|
case "keys": {
|
|
@@ -533,14 +1065,133 @@ function cleanupNappState(pubkey, dTag, aggregateHash, statePersistence) {
|
|
|
533
1065
|
statePersistence.clear(legacyPrefix);
|
|
534
1066
|
}
|
|
535
1067
|
|
|
1068
|
+
// src/domain-handlers.ts
|
|
1069
|
+
var THEME_FALLBACK_DEFAULT = {
|
|
1070
|
+
colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
|
|
1071
|
+
};
|
|
1072
|
+
function createRuntimeDomainHandlers(context) {
|
|
1073
|
+
return {
|
|
1074
|
+
storage: (windowId, msg) => handleStorageMessage(context, windowId, msg),
|
|
1075
|
+
media: (windowId, msg) => handleMediaMessage(context, windowId, msg),
|
|
1076
|
+
keys: (windowId, msg) => handleKeysMessage(context, windowId, msg),
|
|
1077
|
+
notify: (windowId, msg) => handleNotifyMessage(context, windowId, msg),
|
|
1078
|
+
theme: (windowId, msg) => handleThemeMessage(context, windowId, msg),
|
|
1079
|
+
config: (windowId, msg) => handleServiceOnlyMessage(context, "config", windowId, msg),
|
|
1080
|
+
resource: (windowId, msg) => handleServiceOnlyMessage(context, "resource", windowId, msg),
|
|
1081
|
+
cvm: (windowId, msg) => handleServiceOnlyMessage(context, "cvm", windowId, msg),
|
|
1082
|
+
outbox: (windowId, msg) => handleServiceOnlyMessage(context, "outbox", windowId, msg)
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function handleStorageMessage(context, windowId, msg) {
|
|
1086
|
+
const { aclState, hooks, sessionRegistry } = context;
|
|
1087
|
+
handleStorageNub(windowId, msg, hooks.sendToNapplet, sessionRegistry, aclState, hooks.statePersistence);
|
|
1088
|
+
}
|
|
1089
|
+
function handleMediaMessage(context, windowId, msg) {
|
|
1090
|
+
const { hooks, serviceRegistry } = context;
|
|
1091
|
+
const mediaService = serviceRegistry["media"];
|
|
1092
|
+
if (mediaService) {
|
|
1093
|
+
mediaService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (msg.type === "media.session.create") {
|
|
1097
|
+
const m = msg;
|
|
1098
|
+
if (m.owner !== "napplet" && m.owner !== "shell") {
|
|
1099
|
+
hooks.sendToNapplet(windowId, {
|
|
1100
|
+
type: "media.session.create.result",
|
|
1101
|
+
id: m.id ?? "",
|
|
1102
|
+
error: "missing owner"
|
|
1103
|
+
});
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (m.owner === "shell") {
|
|
1107
|
+
hooks.sendToNapplet(windowId, {
|
|
1108
|
+
type: "media.session.create.result",
|
|
1109
|
+
id: m.id ?? "",
|
|
1110
|
+
owner: "shell",
|
|
1111
|
+
error: "unsupported owner mode"
|
|
1112
|
+
});
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
hooks.sendToNapplet(windowId, {
|
|
1116
|
+
type: "media.session.create.result",
|
|
1117
|
+
id: m.id ?? "",
|
|
1118
|
+
sessionId: m.sessionId ?? "",
|
|
1119
|
+
owner: m.owner
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function handleKeysMessage(context, windowId, msg) {
|
|
1124
|
+
const { hooks, serviceRegistry } = context;
|
|
1125
|
+
const keysService = serviceRegistry["keys"];
|
|
1126
|
+
if (keysService) {
|
|
1127
|
+
keysService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (msg.type === "keys.forward") {
|
|
1131
|
+
forwardHotkey(hooks, msg);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (msg.type === "keys.registerAction") sendRegisterActionResult(hooks, windowId, msg);
|
|
1135
|
+
}
|
|
1136
|
+
function forwardHotkey(hooks, msg) {
|
|
1137
|
+
const m = msg;
|
|
1138
|
+
hooks.hotkeys.executeHotkeyFromForward({
|
|
1139
|
+
key: m.key ?? "",
|
|
1140
|
+
code: m.code ?? "",
|
|
1141
|
+
ctrlKey: !!m.ctrl,
|
|
1142
|
+
altKey: !!m.alt,
|
|
1143
|
+
shiftKey: !!m.shift,
|
|
1144
|
+
metaKey: !!m.meta
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function sendRegisterActionResult(hooks, windowId, msg) {
|
|
1148
|
+
const m = msg;
|
|
1149
|
+
hooks.sendToNapplet(windowId, {
|
|
1150
|
+
type: "keys.registerAction.result",
|
|
1151
|
+
id: m.id ?? "",
|
|
1152
|
+
actionId: m.action?.id ?? "",
|
|
1153
|
+
...m.action?.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function handleNotifyMessage(context, windowId, msg) {
|
|
1157
|
+
const { hooks, serviceRegistry } = context;
|
|
1158
|
+
const notifyService = serviceRegistry["notify"];
|
|
1159
|
+
if (notifyService) {
|
|
1160
|
+
notifyService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (msg.type === "notify.send") {
|
|
1164
|
+
const m = msg;
|
|
1165
|
+
hooks.sendToNapplet(windowId, { type: "notify.send.result", id: m.id ?? "", notificationId: `shell-${Date.now()}` });
|
|
1166
|
+
} else if (msg.type === "notify.permission.request") {
|
|
1167
|
+
const m = msg;
|
|
1168
|
+
hooks.sendToNapplet(windowId, { type: "notify.permission.result", id: m.id ?? "", granted: true });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function handleThemeMessage(context, windowId, msg) {
|
|
1172
|
+
const { hooks, serviceRegistry } = context;
|
|
1173
|
+
const themeService = serviceRegistry["theme"];
|
|
1174
|
+
if (themeService) {
|
|
1175
|
+
themeService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (msg.type === "theme.get") {
|
|
1179
|
+
const m = msg;
|
|
1180
|
+
hooks.sendToNapplet(windowId, {
|
|
1181
|
+
type: "theme.get.result",
|
|
1182
|
+
id: m.id ?? "",
|
|
1183
|
+
theme: THEME_FALLBACK_DEFAULT
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function handleServiceOnlyMessage(context, name, windowId, msg) {
|
|
1188
|
+
const service = context.serviceRegistry[name];
|
|
1189
|
+
if (!service) return;
|
|
1190
|
+
service.handleMessage(windowId, msg, (resp) => context.hooks.sendToNapplet(windowId, resp));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
536
1193
|
// src/runtime.ts
|
|
537
|
-
function
|
|
538
|
-
const subscriptions = /* @__PURE__ */ new Map();
|
|
539
|
-
const ifcSubscriptions = /* @__PURE__ */ new Map();
|
|
540
|
-
const ifcChannels = /* @__PURE__ */ new Map();
|
|
541
|
-
const ifcChannelsByWindow = /* @__PURE__ */ new Map();
|
|
542
|
-
let _consentHandler = null;
|
|
543
|
-
const serviceRegistry = { ...hooks.services ?? {} };
|
|
1194
|
+
function createRegisteredServices(serviceRegistry) {
|
|
544
1195
|
const registeredServices = /* @__PURE__ */ new Map();
|
|
545
1196
|
for (const [name, handler] of Object.entries(serviceRegistry)) {
|
|
546
1197
|
registeredServices.set(name, {
|
|
@@ -549,824 +1200,37 @@ function createRuntime(hooks) {
|
|
|
549
1200
|
description: handler.descriptor.description
|
|
550
1201
|
});
|
|
551
1202
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const manifestCache = createManifestCache(hooks.manifestPersistence);
|
|
556
|
-
const replayDetector = createReplayDetector(
|
|
557
|
-
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().replayWindowSeconds : void 0
|
|
558
|
-
);
|
|
559
|
-
const enforce = createEnforceGate({
|
|
560
|
-
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
561
|
-
resolveIdentity: (pubkey) => {
|
|
562
|
-
const entry = sessionRegistry.getEntry(pubkey);
|
|
563
|
-
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash } : void 0;
|
|
564
|
-
},
|
|
565
|
-
onAclCheck: hooks.onAclCheck
|
|
566
|
-
});
|
|
567
|
-
const enforceNub = createNubEnforceGate({
|
|
568
|
-
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
569
|
-
resolveIdentityByWindowId: (windowId) => {
|
|
570
|
-
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
571
|
-
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash } : void 0;
|
|
572
|
-
},
|
|
573
|
-
onAclCheck: hooks.onAclCheck
|
|
574
|
-
});
|
|
575
|
-
const eventBuffer = createEventBuffer(
|
|
576
|
-
hooks.sendToNapplet,
|
|
577
|
-
sessionRegistry,
|
|
578
|
-
enforce,
|
|
579
|
-
subscriptions,
|
|
580
|
-
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().ringBufferSize ?? RING_BUFFER_SIZE : void 0
|
|
581
|
-
);
|
|
582
|
-
aclState.load();
|
|
583
|
-
manifestCache.load();
|
|
584
|
-
function checkCompatibility(requires, windowId, eventId) {
|
|
585
|
-
if (requires.length === 0) return true;
|
|
586
|
-
const available = Array.from(registeredServices.values());
|
|
587
|
-
const registeredNames = new Set(registeredServices.keys());
|
|
588
|
-
const missing = requires.filter((name) => !registeredNames.has(name));
|
|
589
|
-
const compatible = missing.length === 0;
|
|
590
|
-
if (!compatible) {
|
|
591
|
-
const report = { available, missing, compatible };
|
|
592
|
-
hooks.onCompatibilityIssue?.(report);
|
|
593
|
-
if (hooks.strictMode) {
|
|
594
|
-
hooks.sendToNapplet(windowId, [
|
|
595
|
-
"OK",
|
|
596
|
-
eventId,
|
|
597
|
-
false,
|
|
598
|
-
`blocked: missing required services: ${missing.join(", ")}`
|
|
599
|
-
]);
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return true;
|
|
604
|
-
}
|
|
605
|
-
function checkUndeclaredService(windowId, pubkey, serviceName, event, onApproved) {
|
|
606
|
-
if (!registeredServices.has(serviceName)) return true;
|
|
607
|
-
const nappletPubkey = sessionRegistry.getPubkey(windowId);
|
|
608
|
-
if (!nappletPubkey) return true;
|
|
609
|
-
const nappletEntry = sessionRegistry.getEntry(nappletPubkey);
|
|
610
|
-
if (!nappletEntry) return true;
|
|
611
|
-
const requires = manifestCache.getRequires(nappletEntry.pubkey, nappletEntry.dTag);
|
|
612
|
-
if (requires.includes(serviceName)) return true;
|
|
613
|
-
const consentKey = `${windowId}:${serviceName}`;
|
|
614
|
-
if (undeclaredServiceConsents.has(consentKey)) return true;
|
|
615
|
-
if (_consentHandler) {
|
|
616
|
-
_consentHandler({
|
|
617
|
-
type: "undeclared-service",
|
|
618
|
-
windowId,
|
|
619
|
-
pubkey,
|
|
620
|
-
event,
|
|
621
|
-
serviceName,
|
|
622
|
-
resolve: (allowed) => {
|
|
623
|
-
if (allowed) {
|
|
624
|
-
undeclaredServiceConsents.add(consentKey);
|
|
625
|
-
onApproved();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
return false;
|
|
630
|
-
}
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
function handleHotkeyForward(event) {
|
|
634
|
-
const keyData = {
|
|
635
|
-
key: event.tags?.find((t) => t[0] === "key")?.[1] ?? "",
|
|
636
|
-
code: event.tags?.find((t) => t[0] === "code")?.[1] ?? "",
|
|
637
|
-
ctrlKey: event.tags?.find((t) => t[0] === "ctrl")?.[1] === "1",
|
|
638
|
-
altKey: event.tags?.find((t) => t[0] === "alt")?.[1] === "1",
|
|
639
|
-
shiftKey: event.tags?.find((t) => t[0] === "shift")?.[1] === "1",
|
|
640
|
-
metaKey: event.tags?.find((t) => t[0] === "meta")?.[1] === "1"
|
|
641
|
-
};
|
|
642
|
-
hooks.hotkeys.executeHotkeyFromForward(keyData);
|
|
643
|
-
}
|
|
644
|
-
function handleShellCommand(event, windowId, topic) {
|
|
645
|
-
function sendOk(success, reason) {
|
|
646
|
-
hooks.sendToNapplet(windowId, ["OK", event.id, success, reason]);
|
|
647
|
-
}
|
|
648
|
-
function sendInterPaneReply(replyTopic, content) {
|
|
649
|
-
const responseEvent = {
|
|
650
|
-
kind: 29e3,
|
|
651
|
-
// IPC_PEER — inlined numeric after Phase 24 shim deletion
|
|
652
|
-
pubkey: "",
|
|
653
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
654
|
-
tags: [["t", replyTopic]],
|
|
655
|
-
content,
|
|
656
|
-
id: "",
|
|
657
|
-
sig: ""
|
|
658
|
-
};
|
|
659
|
-
hooks.sendToNapplet(windowId, ["EVENT", "__shell__", responseEvent]);
|
|
660
|
-
sendOk(true, "");
|
|
661
|
-
}
|
|
662
|
-
switch (topic) {
|
|
663
|
-
case "shell:acl-get": {
|
|
664
|
-
const aclEntries = aclState.getAllEntries();
|
|
665
|
-
const nappletEntries = sessionRegistry.getAllEntries();
|
|
666
|
-
const nappletInfoMap = {};
|
|
667
|
-
for (const e of nappletEntries) nappletInfoMap[e.pubkey] = { type: e.type, registeredAt: e.registeredAt };
|
|
668
|
-
const merged = [...aclEntries];
|
|
669
|
-
for (const e of nappletEntries) {
|
|
670
|
-
if (!merged.find((a) => a.pubkey === e.pubkey)) {
|
|
671
|
-
merged.push({ pubkey: e.pubkey, capabilities: [...ALL_CAPABILITIES], blocked: false });
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
const display = merged.map((e) => ({
|
|
675
|
-
...e,
|
|
676
|
-
type: nappletInfoMap[e.pubkey]?.type ?? "unknown",
|
|
677
|
-
registeredAt: nappletInfoMap[e.pubkey]?.registeredAt ?? 0
|
|
678
|
-
}));
|
|
679
|
-
sendInterPaneReply("shell:acl-current", JSON.stringify({ entries: display }));
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
case "shell:acl-revoke":
|
|
683
|
-
case "shell:acl-grant":
|
|
684
|
-
case "shell:acl-block":
|
|
685
|
-
case "shell:acl-unblock": {
|
|
686
|
-
const pk = event.tags?.find((t) => t[0] === "pubkey")?.[1];
|
|
687
|
-
const cap = event.tags?.find((t) => t[0] === "cap")?.[1];
|
|
688
|
-
if (!pk) {
|
|
689
|
-
sendOk(false, "error: missing pubkey tag");
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
const ne = sessionRegistry.getEntry(pk);
|
|
693
|
-
if (topic === "shell:acl-revoke" && cap) aclState.revoke(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "", cap);
|
|
694
|
-
else if (topic === "shell:acl-grant" && cap) aclState.grant(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "", cap);
|
|
695
|
-
else if (topic === "shell:acl-block") aclState.block(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "");
|
|
696
|
-
else if (topic === "shell:acl-unblock") aclState.unblock(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "");
|
|
697
|
-
aclState.persist();
|
|
698
|
-
const ae = aclState.getEntry(pk, ne?.dTag ?? "", ne?.aggregateHash ?? "");
|
|
699
|
-
sendInterPaneReply("shell:acl-current", JSON.stringify({ entries: ae ? [ae] : [] }));
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
case "shell:relay-get":
|
|
703
|
-
sendInterPaneReply("shell:relay-current", JSON.stringify(hooks.relayConfig.getRelayConfig()));
|
|
704
|
-
break;
|
|
705
|
-
case "shell:relay-add": {
|
|
706
|
-
const tier = event.tags?.find((t) => t[0] === "tier")?.[1];
|
|
707
|
-
const url = event.tags?.find((t) => t[0] === "url")?.[1];
|
|
708
|
-
if (tier && url) {
|
|
709
|
-
hooks.relayConfig.addRelay(tier, url);
|
|
710
|
-
sendInterPaneReply("shell:relay-current", JSON.stringify(hooks.relayConfig.getRelayConfig()));
|
|
711
|
-
} else sendOk(false, "error: missing tier/url");
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
case "shell:relay-remove": {
|
|
715
|
-
const tier = event.tags?.find((t) => t[0] === "tier")?.[1];
|
|
716
|
-
const url = event.tags?.find((t) => t[0] === "url")?.[1];
|
|
717
|
-
if (tier && url) {
|
|
718
|
-
hooks.relayConfig.removeRelay(tier, url);
|
|
719
|
-
sendInterPaneReply("shell:relay-current", JSON.stringify(hooks.relayConfig.getRelayConfig()));
|
|
720
|
-
} else sendOk(false, "error: missing tier/url");
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
case "shell:relay-nip66":
|
|
724
|
-
sendInterPaneReply("shell:relay-nip66-data", JSON.stringify(hooks.relayConfig.getNip66Suggestions()));
|
|
725
|
-
break;
|
|
726
|
-
case "shell:relay-scoped-connect": {
|
|
727
|
-
const url = event.tags?.find((t) => t[0] === "url")?.[1];
|
|
728
|
-
const subId = event.tags?.find((t) => t[0] === "sub-id")?.[1];
|
|
729
|
-
const filtersTag = event.tags?.find((t) => t[0] === "filters")?.[1];
|
|
730
|
-
if (!url || !subId || !filtersTag) {
|
|
731
|
-
sendOk(false, "error: missing tags");
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
try {
|
|
735
|
-
const filters = JSON.parse(filtersTag);
|
|
736
|
-
hooks.relayPool?.openScopedRelay(windowId, url, subId, filters, hooks.sendToNapplet);
|
|
737
|
-
sendOk(true, "");
|
|
738
|
-
} catch {
|
|
739
|
-
sendOk(false, "error: invalid filters");
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case "shell:relay-scoped-close":
|
|
744
|
-
hooks.relayPool?.closeScopedRelay(windowId);
|
|
745
|
-
sendOk(true, "");
|
|
746
|
-
break;
|
|
747
|
-
case "shell:relay-scoped-publish": {
|
|
748
|
-
const et = event.tags?.find((t) => t[0] === "event")?.[1];
|
|
749
|
-
if (!et) {
|
|
750
|
-
sendOk(false, "error: missing event tag");
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
try {
|
|
754
|
-
const signed = JSON.parse(et);
|
|
755
|
-
const ok = hooks.relayPool?.publishToScopedRelay(windowId, signed) ?? false;
|
|
756
|
-
sendOk(ok, ok ? "" : "error: no active scoped relay");
|
|
757
|
-
} catch {
|
|
758
|
-
sendOk(false, "error: invalid event JSON");
|
|
759
|
-
}
|
|
760
|
-
break;
|
|
761
|
-
}
|
|
762
|
-
case "shell:create-window": {
|
|
763
|
-
try {
|
|
764
|
-
const payload = JSON.parse(event.content);
|
|
765
|
-
if (!payload.title || !payload.class) {
|
|
766
|
-
sendOk(false, "error: requires title and class");
|
|
767
|
-
break;
|
|
768
|
-
}
|
|
769
|
-
const id = hooks.windowManager.createWindow({ title: payload.title, class: payload.class, iframeSrc: payload.iframeSrc });
|
|
770
|
-
sendOk(!!id, id ? "" : "error: window creation failed");
|
|
771
|
-
} catch {
|
|
772
|
-
sendOk(false, "error: invalid JSON");
|
|
773
|
-
}
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
case "shell:send-dm": {
|
|
777
|
-
if (hooks.dm) {
|
|
778
|
-
const corrId = event.tags?.find((t) => t[0] === "id")?.[1] ?? "";
|
|
779
|
-
const recipient = event.tags?.find((t) => t[0] === "p")?.[1];
|
|
780
|
-
let message;
|
|
781
|
-
try {
|
|
782
|
-
message = JSON.parse(event.content).message;
|
|
783
|
-
} catch {
|
|
784
|
-
}
|
|
785
|
-
if (!recipient || !message) {
|
|
786
|
-
sendOk(false, "error: missing recipient or message");
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
hooks.dm.sendDm(recipient, message).then((result) => {
|
|
790
|
-
const payload = result.success ? { success: true, ...result.eventId ? { eventId: result.eventId } : {} } : { success: false, error: result.error ?? "unknown error" };
|
|
791
|
-
const response = {
|
|
792
|
-
kind: 29e3,
|
|
793
|
-
// IPC_PEER — inlined numeric after Phase 24 shim deletion
|
|
794
|
-
pubkey: "",
|
|
795
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
796
|
-
tags: [["t", "shell:send-dm-result"], ["id", corrId]],
|
|
797
|
-
content: JSON.stringify(payload),
|
|
798
|
-
id: "",
|
|
799
|
-
sig: ""
|
|
800
|
-
};
|
|
801
|
-
hooks.sendToNapplet(windowId, ["EVENT", "__shell__", response]);
|
|
802
|
-
sendOk(result.success, result.success ? "" : `error: ${result.error}`);
|
|
803
|
-
}).catch(() => {
|
|
804
|
-
sendOk(false, "error: DM send failed");
|
|
805
|
-
});
|
|
806
|
-
} else sendOk(false, "error: DM hooks not configured");
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
default:
|
|
810
|
-
sendOk(true, "");
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
function handleRelayMessage(windowId, msg) {
|
|
815
|
-
const m = msg;
|
|
816
|
-
const dotIdx = msg.type.indexOf(".");
|
|
817
|
-
const action = msg.type.slice(dotIdx + 1);
|
|
818
|
-
switch (action) {
|
|
819
|
-
case "subscribe": {
|
|
820
|
-
let deliver2 = function(event) {
|
|
821
|
-
if (seenIds.has(event.id)) return;
|
|
822
|
-
seenIds.add(event.id);
|
|
823
|
-
if (subscriptions.has(subKey)) {
|
|
824
|
-
hooks.sendToNapplet(windowId, { type: "relay.event", subId, event });
|
|
825
|
-
}
|
|
826
|
-
};
|
|
827
|
-
var deliver = deliver2;
|
|
828
|
-
const subId = m.subId ?? "";
|
|
829
|
-
const filters = m.filters ?? [];
|
|
830
|
-
if (!subId) return;
|
|
831
|
-
const subKey = `${windowId}:${subId}`;
|
|
832
|
-
subscriptions.set(subKey, { windowId, filters });
|
|
833
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
834
|
-
for (const bufferedEvent of eventBuffer.getBufferedEvents()) {
|
|
835
|
-
if (matchesAnyFilter(bufferedEvent, filters)) deliver2(bufferedEvent);
|
|
836
|
-
}
|
|
837
|
-
const isShellKind = filters.length > 0 && filters.every((f) => f.kinds?.every((k) => k >= 29e3 && k < 3e4));
|
|
838
|
-
if (!isShellKind) {
|
|
839
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
840
|
-
const cacheService = !serviceRegistry["relay"] ? serviceRegistry["cache"] : void 0;
|
|
841
|
-
if (relayService) {
|
|
842
|
-
relayService.handleMessage(windowId, msg, (resp) => {
|
|
843
|
-
if (!subscriptions.has(subKey)) return;
|
|
844
|
-
hooks.sendToNapplet(windowId, resp);
|
|
845
|
-
});
|
|
846
|
-
if (cacheService) cacheService.handleMessage(windowId, msg, (resp) => {
|
|
847
|
-
if (!subscriptions.has(subKey)) return;
|
|
848
|
-
hooks.sendToNapplet(windowId, resp);
|
|
849
|
-
});
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
const cache = hooks.cache;
|
|
854
|
-
if (cache?.isAvailable() && !isShellKind) {
|
|
855
|
-
cache.query(filters).then((cachedEvents) => {
|
|
856
|
-
for (const event of cachedEvents) deliver2(event);
|
|
857
|
-
}).catch(() => {
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
const pool = hooks.relayPool;
|
|
861
|
-
if (pool?.isAvailable() && !isShellKind) {
|
|
862
|
-
const relayUrls = pool.selectRelayTier(filters);
|
|
863
|
-
let eoseSent = false;
|
|
864
|
-
const eoseFallbackTimer = setTimeout(() => {
|
|
865
|
-
if (!eoseSent) {
|
|
866
|
-
eoseSent = true;
|
|
867
|
-
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
868
|
-
}
|
|
869
|
-
}, 15e3);
|
|
870
|
-
const subscription = pool.subscribe(filters, (item) => {
|
|
871
|
-
if (item === "EOSE") {
|
|
872
|
-
clearTimeout(eoseFallbackTimer);
|
|
873
|
-
if (!eoseSent) {
|
|
874
|
-
eoseSent = true;
|
|
875
|
-
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
876
|
-
}
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
deliver2(item);
|
|
880
|
-
if (cache?.isAvailable() && !isShellKind) {
|
|
881
|
-
try {
|
|
882
|
-
cache.store(item);
|
|
883
|
-
} catch {
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}, relayUrls);
|
|
887
|
-
pool.trackSubscription(subKey, () => {
|
|
888
|
-
clearTimeout(eoseFallbackTimer);
|
|
889
|
-
subscription.unsubscribe();
|
|
890
|
-
});
|
|
891
|
-
} else if (!isShellKind) {
|
|
892
|
-
hooks.sendToNapplet(windowId, { type: "relay.eose", subId });
|
|
893
|
-
}
|
|
894
|
-
break;
|
|
895
|
-
}
|
|
896
|
-
case "close": {
|
|
897
|
-
const subId = m.subId ?? "";
|
|
898
|
-
if (!subId) return;
|
|
899
|
-
const subKey = `${windowId}:${subId}`;
|
|
900
|
-
subscriptions.delete(subKey);
|
|
901
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
902
|
-
if (relayService) {
|
|
903
|
-
relayService.handleMessage(windowId, msg, () => {
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
hooks.relayPool?.untrackSubscription(subKey);
|
|
907
|
-
hooks.sendToNapplet(windowId, { type: "relay.closed", subId, message: "" });
|
|
908
|
-
break;
|
|
909
|
-
}
|
|
910
|
-
case "publish": {
|
|
911
|
-
const event = m.event;
|
|
912
|
-
const id = m.id ?? "";
|
|
913
|
-
if (!event || typeof event !== "object") {
|
|
914
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.error", id, error: "invalid event" });
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const replayResult = replayDetector.check(event);
|
|
918
|
-
if (replayResult !== null) {
|
|
919
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: replayResult });
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
923
|
-
if (relayService) {
|
|
924
|
-
relayService.handleMessage(windowId, msg, (resp) => {
|
|
925
|
-
hooks.sendToNapplet(windowId, resp);
|
|
926
|
-
});
|
|
927
|
-
} else if (hooks.relayPool?.isAvailable()) {
|
|
928
|
-
hooks.relayPool.publish(event);
|
|
929
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: true });
|
|
930
|
-
} else {
|
|
931
|
-
hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: "no relay pool available" });
|
|
932
|
-
}
|
|
933
|
-
eventBuffer.bufferAndDeliver(event, windowId);
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
// Shell-mediated encryption path (NUB-08 / SH-C03). Napplets hand the
|
|
937
|
-
// shell a plaintext EventTemplate plus a recipient pubkey; the shell
|
|
938
|
-
// encrypts via its own signer (nip44 default, nip04 opt-in), signs,
|
|
939
|
-
// publishes, and returns a relay.publishEncrypted.result. The signer's
|
|
940
|
-
// nip04/nip44 primitives are SHELL-INTERNAL — no napplet-visible
|
|
941
|
-
// message surface reaches them (SignerProxy was deleted in Plan 12-01).
|
|
942
|
-
case "publishEncrypted": {
|
|
943
|
-
let replyPe2 = function(ok, extra = {}) {
|
|
944
|
-
hooks.sendToNapplet(windowId, {
|
|
945
|
-
type: "relay.publishEncrypted.result",
|
|
946
|
-
id,
|
|
947
|
-
ok,
|
|
948
|
-
...extra
|
|
949
|
-
});
|
|
950
|
-
};
|
|
951
|
-
var replyPe = replyPe2;
|
|
952
|
-
const id = m.id ?? "";
|
|
953
|
-
const eventTemplate = m.event;
|
|
954
|
-
const peMsg = msg;
|
|
955
|
-
const recipient = peMsg.recipient ?? "";
|
|
956
|
-
const encryption = peMsg.encryption ?? "nip44";
|
|
957
|
-
if (!recipient) {
|
|
958
|
-
replyPe2(false, { error: "missing recipient" });
|
|
959
|
-
break;
|
|
960
|
-
}
|
|
961
|
-
if (encryption !== "nip44" && encryption !== "nip04") {
|
|
962
|
-
replyPe2(false, { error: `unsupported encryption scheme: ${encryption}` });
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
965
|
-
const peSigner = hooks.auth.getSigner();
|
|
966
|
-
if (!peSigner) {
|
|
967
|
-
replyPe2(false, { error: "no signer configured" });
|
|
968
|
-
break;
|
|
969
|
-
}
|
|
970
|
-
if (!eventTemplate || typeof eventTemplate !== "object") {
|
|
971
|
-
replyPe2(false, { error: "invalid event template" });
|
|
972
|
-
break;
|
|
973
|
-
}
|
|
974
|
-
(async () => {
|
|
975
|
-
try {
|
|
976
|
-
const plaintext = String(eventTemplate.content ?? "");
|
|
977
|
-
const ciphertext = encryption === "nip44" ? await peSigner.nip44?.encrypt(recipient, plaintext) ?? "" : await peSigner.nip04?.encrypt(recipient, plaintext) ?? "";
|
|
978
|
-
const eventWithCiphertext = { ...eventTemplate, content: ciphertext };
|
|
979
|
-
const signed = await peSigner.signEvent?.(eventWithCiphertext);
|
|
980
|
-
if (!signed) {
|
|
981
|
-
replyPe2(false, { error: "signEvent returned null" });
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
const relayService = serviceRegistry["relay"] ?? serviceRegistry["relay-pool"];
|
|
985
|
-
if (relayService) {
|
|
986
|
-
const publishMsg = { type: "relay.publish", id, event: signed };
|
|
987
|
-
let replied = false;
|
|
988
|
-
relayService.handleMessage(windowId, publishMsg, (resp) => {
|
|
989
|
-
if (replied) return;
|
|
990
|
-
const r = resp;
|
|
991
|
-
if (typeof r.type === "string" && r.type.startsWith("relay.publish")) {
|
|
992
|
-
const okVal = r.ok ?? r.accepted ?? false;
|
|
993
|
-
replied = true;
|
|
994
|
-
replyPe2(okVal, {
|
|
995
|
-
event: signed,
|
|
996
|
-
eventId: signed.id,
|
|
997
|
-
...okVal ? {} : { error: r.error ?? r.message ?? "publish failed" }
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
if (!replied) {
|
|
1002
|
-
replied = true;
|
|
1003
|
-
replyPe2(true, { event: signed, eventId: signed.id });
|
|
1004
|
-
}
|
|
1005
|
-
} else if (hooks.relayPool?.isAvailable()) {
|
|
1006
|
-
hooks.relayPool.publish(signed);
|
|
1007
|
-
replyPe2(true, { event: signed, eventId: signed.id });
|
|
1008
|
-
} else {
|
|
1009
|
-
replyPe2(false, { error: "no relay pool available" });
|
|
1010
|
-
}
|
|
1011
|
-
try {
|
|
1012
|
-
eventBuffer.bufferAndDeliver(signed, windowId);
|
|
1013
|
-
} catch {
|
|
1014
|
-
}
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
replyPe2(false, { error: err?.message ?? "encryption failed" });
|
|
1017
|
-
}
|
|
1018
|
-
})();
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
case "query": {
|
|
1022
|
-
const id = m.id ?? "";
|
|
1023
|
-
const filters = m.filters ?? [];
|
|
1024
|
-
let count = 0;
|
|
1025
|
-
for (const event of eventBuffer.getBufferedEvents()) {
|
|
1026
|
-
if (matchesAnyFilter(event, filters)) count++;
|
|
1027
|
-
}
|
|
1028
|
-
hooks.sendToNapplet(windowId, { type: "relay.query.result", id, count });
|
|
1029
|
-
break;
|
|
1030
|
-
}
|
|
1031
|
-
default:
|
|
1032
|
-
break;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
function handleIdentityMessage(windowId, msg) {
|
|
1036
|
-
const identityService = serviceRegistry["identity"];
|
|
1037
|
-
if (identityService) {
|
|
1038
|
-
identityService.handleMessage(windowId, msg, (resp) => {
|
|
1039
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1040
|
-
});
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
const id = msg.id ?? "";
|
|
1044
|
-
const action = msg.type.slice("identity.".length);
|
|
1045
|
-
const signer = hooks.auth.getSigner();
|
|
1046
|
-
function sendError(error) {
|
|
1047
|
-
hooks.sendToNapplet(windowId, { type: `${msg.type}.error`, id, error });
|
|
1048
|
-
}
|
|
1049
|
-
function sendResult(payload) {
|
|
1050
|
-
hooks.sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
1051
|
-
}
|
|
1052
|
-
switch (action) {
|
|
1053
|
-
case "getPublicKey": {
|
|
1054
|
-
if (!signer) {
|
|
1055
|
-
sendError("no signer configured");
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey })).catch((err) => sendError(err?.message ?? "getPublicKey failed"));
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
case "getRelays": {
|
|
1062
|
-
if (!signer) {
|
|
1063
|
-
sendError("no signer configured");
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch((err) => sendError(err?.message ?? "getRelays failed"));
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
case "getProfile":
|
|
1070
|
-
sendResult({ profile: null });
|
|
1071
|
-
return;
|
|
1072
|
-
case "getFollows":
|
|
1073
|
-
sendResult({ pubkeys: [] });
|
|
1074
|
-
return;
|
|
1075
|
-
case "getList":
|
|
1076
|
-
sendResult({ entries: [] });
|
|
1077
|
-
return;
|
|
1078
|
-
case "getZaps":
|
|
1079
|
-
sendResult({ zaps: [] });
|
|
1080
|
-
return;
|
|
1081
|
-
case "getMutes":
|
|
1082
|
-
sendResult({ pubkeys: [] });
|
|
1083
|
-
return;
|
|
1084
|
-
case "getBlocked":
|
|
1085
|
-
sendResult({ pubkeys: [] });
|
|
1086
|
-
return;
|
|
1087
|
-
case "getBadges":
|
|
1088
|
-
sendResult({ badges: [] });
|
|
1089
|
-
return;
|
|
1090
|
-
default:
|
|
1091
|
-
sendError(`Unknown identity action: ${action}`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
function handleStorageMessage(windowId, msg) {
|
|
1095
|
-
handleStorageNub(windowId, msg, hooks.sendToNapplet, sessionRegistry, aclState, hooks.statePersistence);
|
|
1096
|
-
}
|
|
1097
|
-
function ifcAddChannel(channelId, peerA, peerB) {
|
|
1098
|
-
ifcChannels.set(channelId, { channelId, peerA, peerB });
|
|
1099
|
-
for (const w of [peerA, peerB]) {
|
|
1100
|
-
let set = ifcChannelsByWindow.get(w);
|
|
1101
|
-
if (!set) {
|
|
1102
|
-
set = /* @__PURE__ */ new Set();
|
|
1103
|
-
ifcChannelsByWindow.set(w, set);
|
|
1104
|
-
}
|
|
1105
|
-
set.add(channelId);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
function ifcRemoveChannel(channelId) {
|
|
1109
|
-
const ch = ifcChannels.get(channelId);
|
|
1110
|
-
if (!ch) return;
|
|
1111
|
-
ifcChannels.delete(channelId);
|
|
1112
|
-
for (const w of [ch.peerA, ch.peerB]) {
|
|
1113
|
-
const set = ifcChannelsByWindow.get(w);
|
|
1114
|
-
if (set) {
|
|
1115
|
-
set.delete(channelId);
|
|
1116
|
-
if (set.size === 0) ifcChannelsByWindow.delete(w);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
function ifcPeerOf(channelId, self) {
|
|
1121
|
-
const ch = ifcChannels.get(channelId);
|
|
1122
|
-
if (!ch) return null;
|
|
1123
|
-
if (ch.peerA === self) return ch.peerB;
|
|
1124
|
-
if (ch.peerB === self) return ch.peerA;
|
|
1125
|
-
return null;
|
|
1126
|
-
}
|
|
1127
|
-
function ifcGenerateChannelId() {
|
|
1128
|
-
return hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
|
|
1129
|
-
}
|
|
1130
|
-
function ifcResolveTarget(target) {
|
|
1131
|
-
if (sessionRegistry.getEntryByWindowId(target)) return target;
|
|
1132
|
-
const entries = sessionRegistry.getAllEntries();
|
|
1133
|
-
const byPubkey = entries.find((e) => e.pubkey === target);
|
|
1134
|
-
return byPubkey?.windowId ?? null;
|
|
1135
|
-
}
|
|
1136
|
-
function handleIfcMessage(windowId, msg) {
|
|
1137
|
-
const m = msg;
|
|
1138
|
-
const dotIdx = msg.type.indexOf(".");
|
|
1139
|
-
const action = msg.type.slice(dotIdx + 1);
|
|
1140
|
-
switch (action) {
|
|
1141
|
-
case "emit": {
|
|
1142
|
-
const topic = m.topic ?? "";
|
|
1143
|
-
const payload = m.payload;
|
|
1144
|
-
if (!topic) return;
|
|
1145
|
-
const subscribers = ifcSubscriptions.get(topic);
|
|
1146
|
-
if (subscribers) {
|
|
1147
|
-
for (const subscriberWindowId of subscribers) {
|
|
1148
|
-
if (subscriberWindowId === windowId) continue;
|
|
1149
|
-
hooks.sendToNapplet(subscriberWindowId, { type: "ifc.event", topic, payload, sender: windowId });
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
case "subscribe": {
|
|
1155
|
-
const id = m.id ?? "";
|
|
1156
|
-
const topic = m.topic ?? "";
|
|
1157
|
-
if (!topic) {
|
|
1158
|
-
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id, error: "missing topic" });
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
let subs = ifcSubscriptions.get(topic);
|
|
1162
|
-
if (!subs) {
|
|
1163
|
-
subs = /* @__PURE__ */ new Set();
|
|
1164
|
-
ifcSubscriptions.set(topic, subs);
|
|
1165
|
-
}
|
|
1166
|
-
subs.add(windowId);
|
|
1167
|
-
hooks.sendToNapplet(windowId, { type: "ifc.subscribe.result", id });
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
case "unsubscribe": {
|
|
1171
|
-
const topic = m.topic ?? "";
|
|
1172
|
-
if (!topic) return;
|
|
1173
|
-
const subs = ifcSubscriptions.get(topic);
|
|
1174
|
-
if (subs) {
|
|
1175
|
-
subs.delete(windowId);
|
|
1176
|
-
if (subs.size === 0) ifcSubscriptions.delete(topic);
|
|
1177
|
-
}
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
case "channel.open": {
|
|
1181
|
-
const id = m.id ?? "";
|
|
1182
|
-
const target = m.target ?? "";
|
|
1183
|
-
const peerWindow = ifcResolveTarget(target);
|
|
1184
|
-
if (!peerWindow) {
|
|
1185
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, error: "target not found" });
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
const channelId = ifcGenerateChannelId();
|
|
1189
|
-
ifcAddChannel(channelId, windowId, peerWindow);
|
|
1190
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.open.result", id, channelId, peer: peerWindow });
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
case "channel.emit": {
|
|
1194
|
-
const channelId = m.channelId ?? "";
|
|
1195
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1196
|
-
if (!peer) return;
|
|
1197
|
-
hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
case "channel.broadcast": {
|
|
1201
|
-
const channels = ifcChannelsByWindow.get(windowId);
|
|
1202
|
-
if (!channels) return;
|
|
1203
|
-
for (const channelId of channels) {
|
|
1204
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1205
|
-
if (peer) hooks.sendToNapplet(peer, { type: "ifc.channel.event", channelId, sender: windowId, payload: m.payload });
|
|
1206
|
-
}
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
case "channel.list": {
|
|
1210
|
-
const id = m.id ?? "";
|
|
1211
|
-
const channels = [];
|
|
1212
|
-
const set = ifcChannelsByWindow.get(windowId);
|
|
1213
|
-
if (set) {
|
|
1214
|
-
for (const channelId of set) {
|
|
1215
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1216
|
-
if (peer) channels.push({ id: channelId, peer });
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.list.result", id, channels });
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
case "channel.close": {
|
|
1223
|
-
const channelId = m.channelId ?? "";
|
|
1224
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1225
|
-
if (!peer) return;
|
|
1226
|
-
hooks.sendToNapplet(windowId, { type: "ifc.channel.closed", channelId });
|
|
1227
|
-
hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
1228
|
-
ifcRemoveChannel(channelId);
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
default:
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
function handleMediaMessage(windowId, msg) {
|
|
1236
|
-
const mediaService = serviceRegistry["media"];
|
|
1237
|
-
if (mediaService) {
|
|
1238
|
-
mediaService.handleMessage(windowId, msg, (resp) => {
|
|
1239
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1240
|
-
});
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if (msg.type === "media.session.create") {
|
|
1244
|
-
const m = msg;
|
|
1245
|
-
hooks.sendToNapplet(windowId, {
|
|
1246
|
-
type: "media.session.create.result",
|
|
1247
|
-
id: m.id ?? "",
|
|
1248
|
-
sessionId: m.sessionId ?? ""
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
function handleKeysMessage(windowId, msg) {
|
|
1253
|
-
const keysService = serviceRegistry["keys"];
|
|
1254
|
-
if (keysService) {
|
|
1255
|
-
keysService.handleMessage(windowId, msg, (resp) => {
|
|
1256
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1257
|
-
});
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
if (msg.type === "keys.forward") {
|
|
1261
|
-
const m = msg;
|
|
1262
|
-
hooks.hotkeys.executeHotkeyFromForward({
|
|
1263
|
-
key: m.key ?? "",
|
|
1264
|
-
code: m.code ?? "",
|
|
1265
|
-
ctrlKey: !!m.ctrl,
|
|
1266
|
-
altKey: !!m.alt,
|
|
1267
|
-
shiftKey: !!m.shift,
|
|
1268
|
-
metaKey: !!m.meta
|
|
1269
|
-
});
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
if (msg.type === "keys.registerAction") {
|
|
1273
|
-
const m = msg;
|
|
1274
|
-
hooks.sendToNapplet(windowId, {
|
|
1275
|
-
type: "keys.registerAction.result",
|
|
1276
|
-
id: m.id ?? "",
|
|
1277
|
-
actionId: m.action?.id ?? "",
|
|
1278
|
-
...m.action?.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
1279
|
-
});
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
function handleNotifyMessage(windowId, msg) {
|
|
1284
|
-
const notifyService = serviceRegistry["notify"];
|
|
1285
|
-
if (notifyService) {
|
|
1286
|
-
notifyService.handleMessage(windowId, msg, (resp) => {
|
|
1287
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1288
|
-
});
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
if (msg.type === "notify.send") {
|
|
1292
|
-
const m = msg;
|
|
1293
|
-
hooks.sendToNapplet(windowId, {
|
|
1294
|
-
type: "notify.send.result",
|
|
1295
|
-
id: m.id ?? "",
|
|
1296
|
-
notificationId: `shell-${Date.now()}`
|
|
1297
|
-
});
|
|
1298
|
-
} else if (msg.type === "notify.permission.request") {
|
|
1299
|
-
const m = msg;
|
|
1300
|
-
hooks.sendToNapplet(windowId, {
|
|
1301
|
-
type: "notify.permission.result",
|
|
1302
|
-
id: m.id ?? "",
|
|
1303
|
-
granted: true
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
const THEME_FALLBACK_DEFAULT = {
|
|
1308
|
-
colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
|
|
1309
|
-
};
|
|
1310
|
-
function handleThemeMessage(windowId, msg) {
|
|
1311
|
-
const themeService = serviceRegistry["theme"];
|
|
1312
|
-
if (themeService) {
|
|
1313
|
-
themeService.handleMessage(windowId, msg, (resp) => {
|
|
1314
|
-
hooks.sendToNapplet(windowId, resp);
|
|
1315
|
-
});
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
if (msg.type === "theme.get") {
|
|
1319
|
-
const m = msg;
|
|
1320
|
-
hooks.sendToNapplet(windowId, {
|
|
1321
|
-
type: "theme.get.result",
|
|
1322
|
-
id: m.id ?? "",
|
|
1323
|
-
theme: THEME_FALLBACK_DEFAULT
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1203
|
+
return registeredServices;
|
|
1204
|
+
}
|
|
1205
|
+
function createNubEnvelopeDispatcher(handlers) {
|
|
1327
1206
|
let currentWindowId = null;
|
|
1328
1207
|
const nubDispatch = createDispatch();
|
|
1329
|
-
const
|
|
1330
|
-
if (currentWindowId
|
|
1331
|
-
handleRelayMessage(currentWindowId, msg);
|
|
1332
|
-
};
|
|
1333
|
-
const identityAdapter = (msg) => {
|
|
1334
|
-
if (currentWindowId === null) return;
|
|
1335
|
-
handleIdentityMessage(currentWindowId, msg);
|
|
1336
|
-
};
|
|
1337
|
-
const keysAdapter = (msg) => {
|
|
1338
|
-
if (currentWindowId === null) return;
|
|
1339
|
-
handleKeysMessage(currentWindowId, msg);
|
|
1340
|
-
};
|
|
1341
|
-
const mediaAdapter = (msg) => {
|
|
1342
|
-
if (currentWindowId === null) return;
|
|
1343
|
-
handleMediaMessage(currentWindowId, msg);
|
|
1344
|
-
};
|
|
1345
|
-
const notifyAdapter = (msg) => {
|
|
1346
|
-
if (currentWindowId === null) return;
|
|
1347
|
-
handleNotifyMessage(currentWindowId, msg);
|
|
1348
|
-
};
|
|
1349
|
-
const storageAdapter = (msg) => {
|
|
1350
|
-
if (currentWindowId === null) return;
|
|
1351
|
-
handleStorageMessage(currentWindowId, msg);
|
|
1352
|
-
};
|
|
1353
|
-
const ifcAdapter = (msg) => {
|
|
1354
|
-
if (currentWindowId === null) return;
|
|
1355
|
-
handleIfcMessage(currentWindowId, msg);
|
|
1208
|
+
const adapt = (handler) => (msg) => {
|
|
1209
|
+
if (currentWindowId !== null) handler(currentWindowId, msg);
|
|
1356
1210
|
};
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1211
|
+
nubDispatch.registerNub("relay", adapt(handlers.relay));
|
|
1212
|
+
nubDispatch.registerNub("identity", adapt(handlers.identity));
|
|
1213
|
+
nubDispatch.registerNub("keys", adapt(handlers.keys));
|
|
1214
|
+
nubDispatch.registerNub("media", adapt(handlers.media));
|
|
1215
|
+
nubDispatch.registerNub("notify", adapt(handlers.notify));
|
|
1216
|
+
nubDispatch.registerNub("storage", adapt(handlers.storage));
|
|
1217
|
+
nubDispatch.registerNub("ifc", adapt(handlers.ifc));
|
|
1218
|
+
nubDispatch.registerNub("theme", adapt(handlers.theme));
|
|
1219
|
+
nubDispatch.registerNub("config", adapt(handlers.config));
|
|
1220
|
+
nubDispatch.registerNub("resource", adapt(handlers.resource));
|
|
1221
|
+
nubDispatch.registerNub("cvm", adapt(handlers.cvm));
|
|
1222
|
+
nubDispatch.registerNub("outbox", adapt(handlers.outbox));
|
|
1223
|
+
return (windowId, envelope) => {
|
|
1224
|
+
currentWindowId = windowId;
|
|
1225
|
+
try {
|
|
1226
|
+
nubDispatch.dispatch(envelope);
|
|
1227
|
+
} finally {
|
|
1228
|
+
currentWindowId = null;
|
|
1229
|
+
}
|
|
1360
1230
|
};
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
nubDispatch.registerNub("media", mediaAdapter);
|
|
1365
|
-
nubDispatch.registerNub("notify", notifyAdapter);
|
|
1366
|
-
nubDispatch.registerNub("storage", storageAdapter);
|
|
1367
|
-
nubDispatch.registerNub("ifc", ifcAdapter);
|
|
1368
|
-
nubDispatch.registerNub("theme", themeAdapter);
|
|
1369
|
-
function handleMessage(windowId, msg) {
|
|
1231
|
+
}
|
|
1232
|
+
function createMessageHandler(hooks, enforceNub, dispatchNubEnvelope) {
|
|
1233
|
+
return (windowId, msg) => {
|
|
1370
1234
|
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
|
|
1371
1235
|
const envelope = msg;
|
|
1372
1236
|
const dotIdx = envelope.type.indexOf(".");
|
|
@@ -1376,47 +1240,62 @@ function createRuntime(hooks) {
|
|
|
1376
1240
|
const result = enforceNub(windowId, caps.senderCap, envelope);
|
|
1377
1241
|
if (!result.allowed) {
|
|
1378
1242
|
const id = envelope.id ?? "";
|
|
1379
|
-
|
|
1243
|
+
const isIdentityDecrypt = envelope.type === "identity.decrypt";
|
|
1244
|
+
const isStorageEnvelope = envelope.type.startsWith("storage.");
|
|
1245
|
+
const error = isIdentityDecrypt ? result.reason === "class-forbidden" ? "class-forbidden" : "policy-denied" : formatDenialReason(result.capability);
|
|
1246
|
+
const type = isStorageEnvelope ? `${envelope.type}.result` : `${envelope.type}.error`;
|
|
1247
|
+
hooks.sendToNapplet(windowId, { type, id, error });
|
|
1380
1248
|
return;
|
|
1381
1249
|
}
|
|
1382
1250
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1251
|
+
dispatchNubEnvelope(windowId, envelope);
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function createInjectedEvent(hooks, topic, payload) {
|
|
1255
|
+
const uuid = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 64).padEnd(64, "0");
|
|
1256
|
+
return {
|
|
1257
|
+
id: uuid,
|
|
1258
|
+
pubkey: "0".repeat(64),
|
|
1259
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1260
|
+
kind: 29e3,
|
|
1261
|
+
tags: [["t", topic]],
|
|
1262
|
+
content: JSON.stringify(payload),
|
|
1263
|
+
sig: "0".repeat(128)
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function createRuntimeInstance(context) {
|
|
1267
|
+
const {
|
|
1268
|
+
aclState,
|
|
1269
|
+
eventBuffer,
|
|
1270
|
+
hooks,
|
|
1271
|
+
ifcRuntime,
|
|
1272
|
+
manifestCache,
|
|
1273
|
+
registeredServices,
|
|
1274
|
+
replayDetector,
|
|
1275
|
+
serviceRegistry,
|
|
1276
|
+
sessionRegistry,
|
|
1277
|
+
subscriptions
|
|
1278
|
+
} = context;
|
|
1279
|
+
const undeclaredServiceConsents = /* @__PURE__ */ new Set();
|
|
1280
|
+
let consentHandler = null;
|
|
1281
|
+
return {
|
|
1282
|
+
handleMessage: context.handleMessage,
|
|
1392
1283
|
injectEvent(topic, payload) {
|
|
1393
|
-
|
|
1394
|
-
const event = {
|
|
1395
|
-
id: uuid,
|
|
1396
|
-
pubkey: "0".repeat(64),
|
|
1397
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
1398
|
-
kind: 29e3,
|
|
1399
|
-
// IPC_PEER — inlined numeric after Phase 24 shim deletion
|
|
1400
|
-
tags: [["t", topic]],
|
|
1401
|
-
content: JSON.stringify(payload),
|
|
1402
|
-
sig: "0".repeat(128)
|
|
1403
|
-
};
|
|
1404
|
-
eventBuffer.bufferAndDeliver(event, null);
|
|
1284
|
+
eventBuffer.bufferAndDeliver(createInjectedEvent(hooks, topic, payload), null);
|
|
1405
1285
|
},
|
|
1406
1286
|
destroy() {
|
|
1407
1287
|
manifestCache.persist();
|
|
1408
1288
|
aclState.persist();
|
|
1409
1289
|
replayDetector.clear();
|
|
1410
1290
|
subscriptions.clear();
|
|
1411
|
-
|
|
1412
|
-
ifcChannels.clear();
|
|
1413
|
-
ifcChannelsByWindow.clear();
|
|
1291
|
+
ifcRuntime.clear();
|
|
1414
1292
|
eventBuffer.clear();
|
|
1415
1293
|
registeredServices.clear();
|
|
1416
1294
|
undeclaredServiceConsents.clear();
|
|
1417
1295
|
},
|
|
1418
1296
|
registerConsentHandler(handler) {
|
|
1419
|
-
|
|
1297
|
+
consentHandler = handler;
|
|
1298
|
+
void consentHandler;
|
|
1420
1299
|
},
|
|
1421
1300
|
registerService(name, handler) {
|
|
1422
1301
|
serviceRegistry[name] = handler;
|
|
@@ -1437,20 +1316,7 @@ function createRuntime(hooks) {
|
|
|
1437
1316
|
hooks.relayPool?.untrackSubscription(key);
|
|
1438
1317
|
}
|
|
1439
1318
|
}
|
|
1440
|
-
|
|
1441
|
-
subs.delete(windowId);
|
|
1442
|
-
if (subs.size === 0) ifcSubscriptions.delete(topic);
|
|
1443
|
-
}
|
|
1444
|
-
const channelIds = ifcChannelsByWindow.get(windowId);
|
|
1445
|
-
if (channelIds) {
|
|
1446
|
-
for (const channelId of [...channelIds]) {
|
|
1447
|
-
const peer = ifcPeerOf(channelId, windowId);
|
|
1448
|
-
if (peer) {
|
|
1449
|
-
hooks.sendToNapplet(peer, { type: "ifc.channel.closed", channelId });
|
|
1450
|
-
}
|
|
1451
|
-
ifcRemoveChannel(channelId);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1319
|
+
ifcRuntime.destroyWindow(windowId);
|
|
1454
1320
|
notifyServiceWindowDestroyed(windowId, serviceRegistry);
|
|
1455
1321
|
},
|
|
1456
1322
|
get sessionRegistry() {
|
|
@@ -1463,7 +1329,64 @@ function createRuntime(hooks) {
|
|
|
1463
1329
|
return manifestCache;
|
|
1464
1330
|
}
|
|
1465
1331
|
};
|
|
1466
|
-
|
|
1332
|
+
}
|
|
1333
|
+
function createRuntime(hooks) {
|
|
1334
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
1335
|
+
const serviceRegistry = { ...hooks.services };
|
|
1336
|
+
const registeredServices = createRegisteredServices(serviceRegistry);
|
|
1337
|
+
const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate);
|
|
1338
|
+
const aclState = createAclState(hooks.aclPersistence);
|
|
1339
|
+
const manifestCache = createManifestCache(hooks.manifestPersistence);
|
|
1340
|
+
const replayDetector = createReplayDetector(
|
|
1341
|
+
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().replayWindowSeconds : void 0
|
|
1342
|
+
);
|
|
1343
|
+
const enforce = createEnforceGate({
|
|
1344
|
+
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
1345
|
+
resolveIdentity: (pubkey) => {
|
|
1346
|
+
const entry = sessionRegistry.getEntry(pubkey);
|
|
1347
|
+
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash } : void 0;
|
|
1348
|
+
},
|
|
1349
|
+
onAclCheck: hooks.onAclCheck
|
|
1350
|
+
});
|
|
1351
|
+
const enforceNub = createNubEnforceGate({
|
|
1352
|
+
checkAcl: (pubkey, dTag, aggregateHash, capability) => aclState.check(pubkey, dTag, aggregateHash, capability),
|
|
1353
|
+
resolveIdentityByWindowId: (windowId) => {
|
|
1354
|
+
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
1355
|
+
return entry ? { dTag: entry.dTag, aggregateHash: entry.aggregateHash, class: entry.class } : void 0;
|
|
1356
|
+
},
|
|
1357
|
+
onAclCheck: hooks.onAclCheck
|
|
1358
|
+
});
|
|
1359
|
+
const eventBuffer = createEventBuffer(
|
|
1360
|
+
hooks.sendToNapplet,
|
|
1361
|
+
sessionRegistry,
|
|
1362
|
+
enforce,
|
|
1363
|
+
subscriptions,
|
|
1364
|
+
hooks.getConfigOverrides ? () => hooks.getConfigOverrides().ringBufferSize ?? RING_BUFFER_SIZE : void 0
|
|
1365
|
+
);
|
|
1366
|
+
aclState.load();
|
|
1367
|
+
manifestCache.load();
|
|
1368
|
+
const ifcRuntime = createIfcRuntime(hooks, sessionRegistry);
|
|
1369
|
+
const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry, aclState });
|
|
1370
|
+
const dispatchNubEnvelope = createNubEnvelopeDispatcher({
|
|
1371
|
+
relay: createRelayHandler({ hooks, serviceRegistry, subscriptions, eventBuffer, replayDetector }),
|
|
1372
|
+
identity: createIdentityHandler({ hooks, serviceRegistry }),
|
|
1373
|
+
ifc: ifcRuntime.handleMessage,
|
|
1374
|
+
...domainHandlers
|
|
1375
|
+
});
|
|
1376
|
+
const handleMessage = createMessageHandler(hooks, enforceNub, dispatchNubEnvelope);
|
|
1377
|
+
return createRuntimeInstance({
|
|
1378
|
+
hooks,
|
|
1379
|
+
serviceRegistry,
|
|
1380
|
+
registeredServices,
|
|
1381
|
+
replayDetector,
|
|
1382
|
+
subscriptions,
|
|
1383
|
+
eventBuffer,
|
|
1384
|
+
ifcRuntime,
|
|
1385
|
+
sessionRegistry,
|
|
1386
|
+
aclState,
|
|
1387
|
+
manifestCache,
|
|
1388
|
+
handleMessage
|
|
1389
|
+
});
|
|
1467
1390
|
}
|
|
1468
1391
|
|
|
1469
1392
|
// src/index.ts
|