@kehto/runtime 0.1.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 +103 -0
- package/dist/index.d.ts +1259 -0
- package/dist/index.js +1492 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
// src/enforce.ts
|
|
2
|
+
import { resolveCapabilitiesNub } from "@kehto/acl";
|
|
3
|
+
function createEnforceGate(config) {
|
|
4
|
+
const { checkAcl, resolveIdentity, onAclCheck } = config;
|
|
5
|
+
return function enforce(pubkey, capability, message) {
|
|
6
|
+
const entry = resolveIdentity(pubkey);
|
|
7
|
+
const dTag = entry?.dTag ?? "";
|
|
8
|
+
const aggregateHash = entry?.aggregateHash ?? "";
|
|
9
|
+
const allowed = checkAcl(pubkey, dTag, aggregateHash, capability);
|
|
10
|
+
const identity = { pubkey, dTag, hash: aggregateHash };
|
|
11
|
+
const decision = allowed ? "allow" : "deny";
|
|
12
|
+
if (onAclCheck) {
|
|
13
|
+
onAclCheck({ identity, capability, decision, message });
|
|
14
|
+
}
|
|
15
|
+
return { allowed, capability };
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function createNubEnforceGate(config) {
|
|
19
|
+
const { checkAcl, resolveIdentityByWindowId, onAclCheck } = config;
|
|
20
|
+
return function enforceNub(windowId, capability, message) {
|
|
21
|
+
const entry = resolveIdentityByWindowId(windowId);
|
|
22
|
+
const dTag = entry?.dTag ?? "";
|
|
23
|
+
const aggregateHash = entry?.aggregateHash ?? "";
|
|
24
|
+
const allowed = checkAcl("", dTag, aggregateHash, capability);
|
|
25
|
+
const identity = { pubkey: "", dTag, hash: aggregateHash };
|
|
26
|
+
const decision = allowed ? "allow" : "deny";
|
|
27
|
+
if (onAclCheck) {
|
|
28
|
+
onAclCheck({ identity, capability, decision, message });
|
|
29
|
+
}
|
|
30
|
+
return { allowed, capability };
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function formatDenialReason(capability) {
|
|
34
|
+
return `denied: ${capability}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/session-registry.ts
|
|
38
|
+
function createSessionRegistry(notifier) {
|
|
39
|
+
const byWindowId = /* @__PURE__ */ new Map();
|
|
40
|
+
const byPubkey = /* @__PURE__ */ new Map();
|
|
41
|
+
const byWindowIdEntry = /* @__PURE__ */ new Map();
|
|
42
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
43
|
+
return {
|
|
44
|
+
register(windowId, entry) {
|
|
45
|
+
byWindowId.set(windowId, entry.pubkey);
|
|
46
|
+
byPubkey.set(entry.pubkey, entry);
|
|
47
|
+
byWindowIdEntry.set(windowId, entry);
|
|
48
|
+
},
|
|
49
|
+
unregister(windowId) {
|
|
50
|
+
const pubkey = byWindowId.get(windowId);
|
|
51
|
+
if (pubkey) {
|
|
52
|
+
byPubkey.delete(pubkey);
|
|
53
|
+
byWindowId.delete(windowId);
|
|
54
|
+
}
|
|
55
|
+
byWindowIdEntry.delete(windowId);
|
|
56
|
+
pendingUpdates.delete(windowId);
|
|
57
|
+
},
|
|
58
|
+
getPubkey(windowId) {
|
|
59
|
+
return byWindowId.get(windowId);
|
|
60
|
+
},
|
|
61
|
+
getEntry(pubkey) {
|
|
62
|
+
return byPubkey.get(pubkey);
|
|
63
|
+
},
|
|
64
|
+
getWindowId(pubkey) {
|
|
65
|
+
return byPubkey.get(pubkey)?.windowId;
|
|
66
|
+
},
|
|
67
|
+
isRegistered(windowId) {
|
|
68
|
+
return byWindowId.has(windowId);
|
|
69
|
+
},
|
|
70
|
+
getAllEntries() {
|
|
71
|
+
return Array.from(byPubkey.values());
|
|
72
|
+
},
|
|
73
|
+
getInstanceId(windowId) {
|
|
74
|
+
const pubkey = byWindowId.get(windowId);
|
|
75
|
+
if (!pubkey) return void 0;
|
|
76
|
+
return byPubkey.get(pubkey)?.instanceId;
|
|
77
|
+
},
|
|
78
|
+
getEntryByWindowId(windowId) {
|
|
79
|
+
return byWindowIdEntry.get(windowId);
|
|
80
|
+
},
|
|
81
|
+
setPendingUpdate(windowId, update) {
|
|
82
|
+
pendingUpdates.set(windowId, update);
|
|
83
|
+
notifier?.(windowId);
|
|
84
|
+
},
|
|
85
|
+
getPendingUpdate(windowId) {
|
|
86
|
+
return pendingUpdates.get(windowId);
|
|
87
|
+
},
|
|
88
|
+
clearPendingUpdate(windowId) {
|
|
89
|
+
pendingUpdates.delete(windowId);
|
|
90
|
+
notifier?.(windowId);
|
|
91
|
+
},
|
|
92
|
+
clear() {
|
|
93
|
+
byWindowId.clear();
|
|
94
|
+
byPubkey.clear();
|
|
95
|
+
byWindowIdEntry.clear();
|
|
96
|
+
pendingUpdates.clear();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
var createNappKeyRegistry = createSessionRegistry;
|
|
101
|
+
|
|
102
|
+
// src/acl-state.ts
|
|
103
|
+
import {
|
|
104
|
+
createState,
|
|
105
|
+
check,
|
|
106
|
+
grant,
|
|
107
|
+
revoke,
|
|
108
|
+
block,
|
|
109
|
+
unblock,
|
|
110
|
+
serialize,
|
|
111
|
+
deserialize,
|
|
112
|
+
getQuota,
|
|
113
|
+
CAP_RELAY_READ,
|
|
114
|
+
CAP_RELAY_WRITE,
|
|
115
|
+
CAP_CACHE_READ,
|
|
116
|
+
CAP_CACHE_WRITE,
|
|
117
|
+
CAP_HOTKEY_FORWARD,
|
|
118
|
+
CAP_STATE_READ,
|
|
119
|
+
CAP_STATE_WRITE,
|
|
120
|
+
CAP_ALL
|
|
121
|
+
} from "@kehto/acl";
|
|
122
|
+
var CAP_IDENTITY_READ = 1 << 5;
|
|
123
|
+
var CAP_KEYS_BIND = 1 << 6;
|
|
124
|
+
var CAP_KEYS_FORWARD = 1 << 7;
|
|
125
|
+
var CAP_MEDIA_CONTROL = 1 << 10;
|
|
126
|
+
var CAP_NOTIFY_SEND = 1 << 11;
|
|
127
|
+
var CAP_NOTIFY_CHANNEL = 1 << 12;
|
|
128
|
+
var CAP_THEME_READ = 1 << 13;
|
|
129
|
+
var CAP_MAP = {
|
|
130
|
+
"relay:read": CAP_RELAY_READ,
|
|
131
|
+
"relay:write": CAP_RELAY_WRITE,
|
|
132
|
+
"cache:read": CAP_CACHE_READ,
|
|
133
|
+
"cache:write": CAP_CACHE_WRITE,
|
|
134
|
+
"hotkey:forward": CAP_HOTKEY_FORWARD,
|
|
135
|
+
"state:read": CAP_STATE_READ,
|
|
136
|
+
"state:write": CAP_STATE_WRITE,
|
|
137
|
+
"identity:read": CAP_IDENTITY_READ,
|
|
138
|
+
"keys:bind": CAP_KEYS_BIND,
|
|
139
|
+
"keys:forward": CAP_KEYS_FORWARD,
|
|
140
|
+
"media:control": CAP_MEDIA_CONTROL,
|
|
141
|
+
"notify:send": CAP_NOTIFY_SEND,
|
|
142
|
+
"notify:channel": CAP_NOTIFY_CHANNEL,
|
|
143
|
+
"theme:read": CAP_THEME_READ
|
|
144
|
+
};
|
|
145
|
+
function capToBit(cap) {
|
|
146
|
+
return CAP_MAP[cap] ?? 0;
|
|
147
|
+
}
|
|
148
|
+
function bitsToCapabilities(bits) {
|
|
149
|
+
const result = [];
|
|
150
|
+
for (const [cap, bit] of Object.entries(CAP_MAP)) {
|
|
151
|
+
if (bits & bit) result.push(cap);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
function toIdentity(pubkey, dTag, hash) {
|
|
156
|
+
return { pubkey, dTag, hash };
|
|
157
|
+
}
|
|
158
|
+
function createAclState(persistence, defaultPolicy = "permissive") {
|
|
159
|
+
let state = createState(defaultPolicy);
|
|
160
|
+
return {
|
|
161
|
+
check(pubkey, dTag, aggregateHash, capability) {
|
|
162
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
163
|
+
return check(state, id, capToBit(capability));
|
|
164
|
+
},
|
|
165
|
+
grant(pubkey, dTag, aggregateHash, capability) {
|
|
166
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
167
|
+
state = grant(state, id, capToBit(capability));
|
|
168
|
+
},
|
|
169
|
+
revoke(pubkey, dTag, aggregateHash, capability) {
|
|
170
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
171
|
+
state = revoke(state, id, capToBit(capability));
|
|
172
|
+
},
|
|
173
|
+
block(pubkey, dTag, aggregateHash) {
|
|
174
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
175
|
+
state = block(state, id);
|
|
176
|
+
},
|
|
177
|
+
unblock(pubkey, dTag, aggregateHash) {
|
|
178
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
179
|
+
state = unblock(state, id);
|
|
180
|
+
},
|
|
181
|
+
isBlocked(pubkey, dTag, aggregateHash) {
|
|
182
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
183
|
+
return !check(state, id, CAP_ALL) && this.getEntry(pubkey, dTag, aggregateHash)?.blocked === true;
|
|
184
|
+
},
|
|
185
|
+
getEntry(pubkey, dTag, aggregateHash) {
|
|
186
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
187
|
+
const key = `${id.dTag}:${id.hash}`;
|
|
188
|
+
const entry = state.entries[key];
|
|
189
|
+
if (!entry) return void 0;
|
|
190
|
+
return {
|
|
191
|
+
pubkey: pubkey || "",
|
|
192
|
+
capabilities: bitsToCapabilities(entry.caps),
|
|
193
|
+
blocked: entry.blocked,
|
|
194
|
+
stateQuota: entry.quota
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
getAllEntries() {
|
|
198
|
+
return Object.entries(state.entries).map(([key, entry]) => {
|
|
199
|
+
const parts = key.split(":");
|
|
200
|
+
return {
|
|
201
|
+
pubkey: "",
|
|
202
|
+
capabilities: bitsToCapabilities(entry.caps),
|
|
203
|
+
blocked: entry.blocked,
|
|
204
|
+
stateQuota: entry.quota
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
getStateQuota(pubkey, dTag, aggregateHash) {
|
|
209
|
+
const id = toIdentity(pubkey, dTag, aggregateHash);
|
|
210
|
+
return getQuota(state, id);
|
|
211
|
+
},
|
|
212
|
+
persist() {
|
|
213
|
+
try {
|
|
214
|
+
persistence.persist(serialize(state));
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
load() {
|
|
219
|
+
try {
|
|
220
|
+
const raw = persistence.load();
|
|
221
|
+
if (!raw) return;
|
|
222
|
+
state = deserialize(raw);
|
|
223
|
+
} catch {
|
|
224
|
+
state = createState(defaultPolicy);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
clear() {
|
|
228
|
+
state = createState(defaultPolicy);
|
|
229
|
+
try {
|
|
230
|
+
persistence.persist("");
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/manifest-cache.ts
|
|
238
|
+
function createManifestCache(persistence) {
|
|
239
|
+
const cache = /* @__PURE__ */ new Map();
|
|
240
|
+
const verificationCache = /* @__PURE__ */ new Map();
|
|
241
|
+
function cacheKey(pubkey, dTag) {
|
|
242
|
+
return `${pubkey}:${dTag}`;
|
|
243
|
+
}
|
|
244
|
+
const self = {
|
|
245
|
+
get(pubkey, dTag) {
|
|
246
|
+
return cache.get(cacheKey(pubkey, dTag));
|
|
247
|
+
},
|
|
248
|
+
set(entry) {
|
|
249
|
+
cache.set(cacheKey(entry.pubkey, entry.dTag), entry);
|
|
250
|
+
self.persist();
|
|
251
|
+
},
|
|
252
|
+
has(pubkey, dTag, hash) {
|
|
253
|
+
const entry = cache.get(cacheKey(pubkey, dTag));
|
|
254
|
+
return !!entry && entry.aggregateHash === hash;
|
|
255
|
+
},
|
|
256
|
+
getRequires(pubkey, dTag) {
|
|
257
|
+
const entry = cache.get(cacheKey(pubkey, dTag));
|
|
258
|
+
return entry?.requires ?? [];
|
|
259
|
+
},
|
|
260
|
+
remove(pubkey, dTag) {
|
|
261
|
+
cache.delete(cacheKey(pubkey, dTag));
|
|
262
|
+
self.persist();
|
|
263
|
+
},
|
|
264
|
+
load() {
|
|
265
|
+
try {
|
|
266
|
+
const raw = persistence.load();
|
|
267
|
+
if (!raw) return;
|
|
268
|
+
const entries = JSON.parse(raw);
|
|
269
|
+
cache.clear();
|
|
270
|
+
for (const [key, val] of entries) cache.set(key, val);
|
|
271
|
+
} catch {
|
|
272
|
+
cache.clear();
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
persist() {
|
|
276
|
+
try {
|
|
277
|
+
persistence.persist(JSON.stringify(Array.from(cache.entries())));
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
clear() {
|
|
282
|
+
cache.clear();
|
|
283
|
+
verificationCache.clear();
|
|
284
|
+
try {
|
|
285
|
+
persistence.persist("");
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
getVerification(eventId) {
|
|
290
|
+
return verificationCache.get(eventId);
|
|
291
|
+
},
|
|
292
|
+
setVerification(eventId, result) {
|
|
293
|
+
verificationCache.set(eventId, result);
|
|
294
|
+
},
|
|
295
|
+
hasVerification(eventId) {
|
|
296
|
+
return verificationCache.has(eventId);
|
|
297
|
+
},
|
|
298
|
+
clearVerifications() {
|
|
299
|
+
verificationCache.clear();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
return self;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/replay.ts
|
|
306
|
+
var REPLAY_WINDOW_SECONDS = 30;
|
|
307
|
+
function createReplayDetector(getReplayWindow) {
|
|
308
|
+
const seenEventIds = /* @__PURE__ */ new Map();
|
|
309
|
+
return {
|
|
310
|
+
check(event) {
|
|
311
|
+
const replayWindow = getReplayWindow?.() ?? REPLAY_WINDOW_SECONDS;
|
|
312
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
313
|
+
if (now - event.created_at > replayWindow) return "invalid: event created_at too old";
|
|
314
|
+
if (event.created_at - now > 10) return "invalid: event created_at in the future";
|
|
315
|
+
if (seenEventIds.has(event.id)) return "duplicate: already processed";
|
|
316
|
+
seenEventIds.set(event.id, now);
|
|
317
|
+
for (const [id, timestamp] of seenEventIds) {
|
|
318
|
+
if (now - timestamp > replayWindow) seenEventIds.delete(id);
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
},
|
|
322
|
+
clear() {
|
|
323
|
+
seenEventIds.clear();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/event-buffer.ts
|
|
329
|
+
var RING_BUFFER_SIZE = 100;
|
|
330
|
+
function matchesFilter(event, filter) {
|
|
331
|
+
if (filter.ids !== void 0 && !filter.ids.some((id) => event.id.startsWith(id))) return false;
|
|
332
|
+
if (filter.authors !== void 0 && !filter.authors.some((a) => event.pubkey.startsWith(a))) return false;
|
|
333
|
+
if (filter.kinds !== void 0 && !filter.kinds.includes(event.kind)) return false;
|
|
334
|
+
if (filter.since !== void 0 && event.created_at < filter.since) return false;
|
|
335
|
+
if (filter.until !== void 0 && event.created_at > filter.until) return false;
|
|
336
|
+
for (const [key, values] of Object.entries(filter)) {
|
|
337
|
+
if (!key.startsWith("#") || values === void 0) continue;
|
|
338
|
+
const tagName = key.slice(1);
|
|
339
|
+
const tagValues = values;
|
|
340
|
+
const eventTagValues = event.tags.filter((t) => t[0] === tagName).map((t) => t[1]);
|
|
341
|
+
if (!tagValues.some((v) => eventTagValues.includes(v))) return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
function matchesAnyFilter(event, filters) {
|
|
346
|
+
if (filters.length === 0) return true;
|
|
347
|
+
return filters.some((filter) => matchesFilter(event, filter));
|
|
348
|
+
}
|
|
349
|
+
function createEventBuffer(sendToNapplet, sessionRegistry, enforce, subscriptions, getBufferSize) {
|
|
350
|
+
const buffer = [];
|
|
351
|
+
function deliverToSubscriptions(event, senderId) {
|
|
352
|
+
const pTag = event.tags?.find((t) => t[0] === "p");
|
|
353
|
+
const targetPubkey = pTag?.[1];
|
|
354
|
+
for (const [subKey, sub] of subscriptions) {
|
|
355
|
+
if (senderId !== null && sub.windowId === senderId) continue;
|
|
356
|
+
const recipientPubkey = sessionRegistry.getPubkey(sub.windowId);
|
|
357
|
+
if (recipientPubkey) {
|
|
358
|
+
const recipientResult = enforce(recipientPubkey, "relay:read", ["EVENT", event]);
|
|
359
|
+
if (!recipientResult.allowed) continue;
|
|
360
|
+
}
|
|
361
|
+
if (targetPubkey) {
|
|
362
|
+
const subPubkey = recipientPubkey;
|
|
363
|
+
if (subPubkey !== targetPubkey) continue;
|
|
364
|
+
}
|
|
365
|
+
if (!matchesAnyFilter(event, sub.filters)) continue;
|
|
366
|
+
const prefix = `${sub.windowId}:`;
|
|
367
|
+
if (!subKey.startsWith(prefix)) continue;
|
|
368
|
+
const subId = subKey.slice(prefix.length);
|
|
369
|
+
sendToNapplet(sub.windowId, ["EVENT", subId, event]);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
bufferAndDeliver(event, senderId) {
|
|
374
|
+
const maxSize = getBufferSize?.() ?? RING_BUFFER_SIZE;
|
|
375
|
+
if (buffer.length >= maxSize) buffer.shift();
|
|
376
|
+
buffer.push(event);
|
|
377
|
+
deliverToSubscriptions(event, senderId);
|
|
378
|
+
},
|
|
379
|
+
deliverToSubscriptions,
|
|
380
|
+
getSubscriptions() {
|
|
381
|
+
return subscriptions;
|
|
382
|
+
},
|
|
383
|
+
getBufferedEvents() {
|
|
384
|
+
return buffer;
|
|
385
|
+
},
|
|
386
|
+
clear() {
|
|
387
|
+
buffer.length = 0;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/runtime.ts
|
|
393
|
+
import { createDispatch } from "@napplet/core";
|
|
394
|
+
import { ALL_CAPABILITIES } from "@kehto/acl/capabilities";
|
|
395
|
+
|
|
396
|
+
// src/service-dispatch.ts
|
|
397
|
+
function routeServiceMessage(windowId, message, services, sendToNapplet) {
|
|
398
|
+
const send = (msg) => sendToNapplet(windowId, msg);
|
|
399
|
+
const domain = message.type.split(".")[0];
|
|
400
|
+
const handler = services[domain];
|
|
401
|
+
if (handler) {
|
|
402
|
+
handler.handleMessage(windowId, message, send);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
if (message.type === "ifc.emit" && typeof message.topic === "string") {
|
|
406
|
+
const prefix = message.topic.split(":")[0];
|
|
407
|
+
const ifcHandler = services[prefix];
|
|
408
|
+
if (ifcHandler) {
|
|
409
|
+
ifcHandler.handleMessage(windowId, message, send);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
function notifyServiceWindowDestroyed(windowId, services) {
|
|
416
|
+
for (const handler of Object.values(services)) {
|
|
417
|
+
try {
|
|
418
|
+
handler.onWindowDestroyed?.(windowId);
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/state-handler.ts
|
|
425
|
+
function scopedKey(dTag, aggregateHash, userKey) {
|
|
426
|
+
return `napplet-state:${dTag}:${aggregateHash}:${userKey}`;
|
|
427
|
+
}
|
|
428
|
+
function legacyScopedKey(pubkey, dTag, aggregateHash, userKey) {
|
|
429
|
+
return `napplet-state:${pubkey}:${dTag}:${aggregateHash}:${userKey}`;
|
|
430
|
+
}
|
|
431
|
+
function byteLength(str) {
|
|
432
|
+
let bytes = 0;
|
|
433
|
+
for (let i = 0; i < str.length; i++) {
|
|
434
|
+
const c = str.charCodeAt(i);
|
|
435
|
+
if (c < 128) bytes += 1;
|
|
436
|
+
else if (c < 2048) bytes += 2;
|
|
437
|
+
else if (c < 55296 || c >= 57344) bytes += 3;
|
|
438
|
+
else {
|
|
439
|
+
i++;
|
|
440
|
+
bytes += 4;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return bytes;
|
|
444
|
+
}
|
|
445
|
+
function handleStorageNub(windowId, msg, sendToNapplet, sessionRegistry, aclState, statePersistence) {
|
|
446
|
+
const m = msg;
|
|
447
|
+
const id = m.id ?? "";
|
|
448
|
+
const action = msg.type.split(".")[1];
|
|
449
|
+
function sendResult(payload) {
|
|
450
|
+
sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
|
|
451
|
+
}
|
|
452
|
+
function sendErrorNub(error) {
|
|
453
|
+
sendToNapplet(windowId, { type: `${msg.type}.error`, id, error });
|
|
454
|
+
}
|
|
455
|
+
const entry = sessionRegistry.getEntryByWindowId(windowId);
|
|
456
|
+
if (!entry) {
|
|
457
|
+
sendErrorNub("not registered");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const { dTag, aggregateHash, pubkey } = entry;
|
|
461
|
+
const prefix = `napplet-state:${dTag}:${aggregateHash}:`;
|
|
462
|
+
const legacyPrefix = pubkey ? `napplet-state:${pubkey}:${dTag}:${aggregateHash}:` : "";
|
|
463
|
+
switch (action) {
|
|
464
|
+
case "get": {
|
|
465
|
+
const key = m.key;
|
|
466
|
+
if (!key) {
|
|
467
|
+
sendErrorNub("missing key");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const newKey = scopedKey(dTag, aggregateHash, key);
|
|
471
|
+
let result = statePersistence.get(newKey);
|
|
472
|
+
if (result === null && pubkey) {
|
|
473
|
+
result = statePersistence.get(legacyScopedKey(pubkey, dTag, aggregateHash, key));
|
|
474
|
+
}
|
|
475
|
+
if (result === null && pubkey) {
|
|
476
|
+
result = statePersistence.get(`napp-state:${pubkey}:${dTag}:${aggregateHash}:${key}`);
|
|
477
|
+
}
|
|
478
|
+
sendResult({ value: result });
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case "set": {
|
|
482
|
+
const key = m.key;
|
|
483
|
+
const value = m.value ?? "";
|
|
484
|
+
if (!key) {
|
|
485
|
+
sendErrorNub("missing key");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const quota = aclState.getStateQuota(pubkey ?? "", dTag, aggregateHash);
|
|
489
|
+
const sk = scopedKey(dTag, aggregateHash, key);
|
|
490
|
+
const newWriteBytes = byteLength(sk + value);
|
|
491
|
+
const existingBytes = statePersistence.calculateBytes(prefix, key);
|
|
492
|
+
if (existingBytes + newWriteBytes > quota) {
|
|
493
|
+
sendErrorNub(`quota exceeded: napplet state limit is ${quota} bytes`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const success = statePersistence.set(sk, value);
|
|
497
|
+
sendResult({ ok: success });
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "remove": {
|
|
501
|
+
const key = m.key;
|
|
502
|
+
if (!key) {
|
|
503
|
+
sendErrorNub("missing key");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
statePersistence.remove(scopedKey(dTag, aggregateHash, key));
|
|
507
|
+
void legacyPrefix;
|
|
508
|
+
sendResult({ ok: true });
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
case "clear": {
|
|
512
|
+
sendErrorNub("storage.clear is not in @napplet/nub-storage; action not supported");
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case "keys": {
|
|
516
|
+
const newKeys = statePersistence.keys(prefix);
|
|
517
|
+
const legacyKeys = legacyPrefix ? statePersistence.keys(legacyPrefix) : [];
|
|
518
|
+
const userKeySet = /* @__PURE__ */ new Set();
|
|
519
|
+
for (const k of newKeys) userKeySet.add(k.startsWith(prefix) ? k.slice(prefix.length) : k);
|
|
520
|
+
for (const k of legacyKeys) userKeySet.add(k.startsWith(legacyPrefix) ? k.slice(legacyPrefix.length) : k);
|
|
521
|
+
sendResult({ keys: Array.from(userKeySet) });
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
default:
|
|
525
|
+
sendErrorNub(`unknown storage action: ${action}`);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function cleanupNappState(pubkey, dTag, aggregateHash, statePersistence) {
|
|
530
|
+
const prefix = `napplet-state:${dTag}:${aggregateHash}:`;
|
|
531
|
+
statePersistence.clear(prefix);
|
|
532
|
+
const legacyPrefix = `napplet-state:${pubkey}:${dTag}:${aggregateHash}:`;
|
|
533
|
+
statePersistence.clear(legacyPrefix);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/runtime.ts
|
|
537
|
+
function createRuntime(hooks) {
|
|
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 ?? {} };
|
|
544
|
+
const registeredServices = /* @__PURE__ */ new Map();
|
|
545
|
+
for (const [name, handler] of Object.entries(serviceRegistry)) {
|
|
546
|
+
registeredServices.set(name, {
|
|
547
|
+
name: handler.descriptor.name,
|
|
548
|
+
version: handler.descriptor.version,
|
|
549
|
+
description: handler.descriptor.description
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const undeclaredServiceConsents = /* @__PURE__ */ new Set();
|
|
553
|
+
const sessionRegistry = createSessionRegistry(hooks.onPendingUpdate);
|
|
554
|
+
const aclState = createAclState(hooks.aclPersistence);
|
|
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
|
+
}
|
|
1327
|
+
let currentWindowId = null;
|
|
1328
|
+
const nubDispatch = createDispatch();
|
|
1329
|
+
const relayAdapter = (msg) => {
|
|
1330
|
+
if (currentWindowId === null) return;
|
|
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);
|
|
1356
|
+
};
|
|
1357
|
+
const themeAdapter = (msg) => {
|
|
1358
|
+
if (currentWindowId === null) return;
|
|
1359
|
+
handleThemeMessage(currentWindowId, msg);
|
|
1360
|
+
};
|
|
1361
|
+
nubDispatch.registerNub("relay", relayAdapter);
|
|
1362
|
+
nubDispatch.registerNub("identity", identityAdapter);
|
|
1363
|
+
nubDispatch.registerNub("keys", keysAdapter);
|
|
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) {
|
|
1370
|
+
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
|
|
1371
|
+
const envelope = msg;
|
|
1372
|
+
const dotIdx = envelope.type.indexOf(".");
|
|
1373
|
+
if (dotIdx === -1) return;
|
|
1374
|
+
const caps = resolveCapabilitiesNub(envelope);
|
|
1375
|
+
if (caps.senderCap) {
|
|
1376
|
+
const result = enforceNub(windowId, caps.senderCap, envelope);
|
|
1377
|
+
if (!result.allowed) {
|
|
1378
|
+
const id = envelope.id ?? "";
|
|
1379
|
+
hooks.sendToNapplet(windowId, { type: `${envelope.type}.error`, id, error: formatDenialReason(result.capability) });
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
currentWindowId = windowId;
|
|
1384
|
+
try {
|
|
1385
|
+
nubDispatch.dispatch(envelope);
|
|
1386
|
+
} finally {
|
|
1387
|
+
currentWindowId = null;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const runtimeInstance = {
|
|
1391
|
+
handleMessage,
|
|
1392
|
+
injectEvent(topic, payload) {
|
|
1393
|
+
const uuid = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 64).padEnd(64, "0");
|
|
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);
|
|
1405
|
+
},
|
|
1406
|
+
destroy() {
|
|
1407
|
+
manifestCache.persist();
|
|
1408
|
+
aclState.persist();
|
|
1409
|
+
replayDetector.clear();
|
|
1410
|
+
subscriptions.clear();
|
|
1411
|
+
ifcSubscriptions.clear();
|
|
1412
|
+
ifcChannels.clear();
|
|
1413
|
+
ifcChannelsByWindow.clear();
|
|
1414
|
+
eventBuffer.clear();
|
|
1415
|
+
registeredServices.clear();
|
|
1416
|
+
undeclaredServiceConsents.clear();
|
|
1417
|
+
},
|
|
1418
|
+
registerConsentHandler(handler) {
|
|
1419
|
+
_consentHandler = handler;
|
|
1420
|
+
},
|
|
1421
|
+
registerService(name, handler) {
|
|
1422
|
+
serviceRegistry[name] = handler;
|
|
1423
|
+
registeredServices.set(name, {
|
|
1424
|
+
name: handler.descriptor.name,
|
|
1425
|
+
version: handler.descriptor.version,
|
|
1426
|
+
description: handler.descriptor.description
|
|
1427
|
+
});
|
|
1428
|
+
},
|
|
1429
|
+
unregisterService(name) {
|
|
1430
|
+
delete serviceRegistry[name];
|
|
1431
|
+
registeredServices.delete(name);
|
|
1432
|
+
},
|
|
1433
|
+
destroyWindow(windowId) {
|
|
1434
|
+
for (const [key] of subscriptions) {
|
|
1435
|
+
if (key.startsWith(`${windowId}:`)) {
|
|
1436
|
+
subscriptions.delete(key);
|
|
1437
|
+
hooks.relayPool?.untrackSubscription(key);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
for (const [topic, subs] of ifcSubscriptions) {
|
|
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
|
+
}
|
|
1454
|
+
notifyServiceWindowDestroyed(windowId, serviceRegistry);
|
|
1455
|
+
},
|
|
1456
|
+
get sessionRegistry() {
|
|
1457
|
+
return sessionRegistry;
|
|
1458
|
+
},
|
|
1459
|
+
get aclState() {
|
|
1460
|
+
return aclState;
|
|
1461
|
+
},
|
|
1462
|
+
get manifestCache() {
|
|
1463
|
+
return manifestCache;
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
return runtimeInstance;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/index.ts
|
|
1470
|
+
import { ALL_CAPABILITIES as ALL_CAPABILITIES2 } from "@kehto/acl/capabilities";
|
|
1471
|
+
export {
|
|
1472
|
+
ALL_CAPABILITIES2 as ALL_CAPABILITIES,
|
|
1473
|
+
RING_BUFFER_SIZE,
|
|
1474
|
+
cleanupNappState,
|
|
1475
|
+
createAclState,
|
|
1476
|
+
createEnforceGate,
|
|
1477
|
+
createEventBuffer,
|
|
1478
|
+
createManifestCache,
|
|
1479
|
+
createNappKeyRegistry,
|
|
1480
|
+
createNubEnforceGate,
|
|
1481
|
+
createReplayDetector,
|
|
1482
|
+
createRuntime,
|
|
1483
|
+
createSessionRegistry,
|
|
1484
|
+
formatDenialReason,
|
|
1485
|
+
handleStorageNub,
|
|
1486
|
+
matchesAnyFilter,
|
|
1487
|
+
matchesFilter,
|
|
1488
|
+
notifyServiceWindowDestroyed,
|
|
1489
|
+
resolveCapabilitiesNub,
|
|
1490
|
+
routeServiceMessage
|
|
1491
|
+
};
|
|
1492
|
+
//# sourceMappingURL=index.js.map
|