@kehto/services 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 +320 -18
- package/dist/cvm-nostr-transport.d.ts +97 -0
- package/dist/cvm-nostr-transport.js +286 -0
- package/dist/cvm-nostr-transport.js.map +1 -0
- package/dist/cvm-service-CXcOVQ0m.d.ts +207 -0
- package/dist/index.d.ts +1065 -76
- package/dist/index.js +1755 -236
- package/dist/index.js.map +1 -1
- package/package.json +19 -22
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var AUDIO_SERVICE_VERSION = "1.0.0";
|
|
|
3
3
|
function createAudioService(options) {
|
|
4
4
|
const sources = /* @__PURE__ */ new Map();
|
|
5
5
|
const onChange = options?.onChange;
|
|
6
|
-
function
|
|
6
|
+
function notify2() {
|
|
7
7
|
onChange?.(new Map(sources));
|
|
8
8
|
}
|
|
9
9
|
const descriptor = {
|
|
@@ -15,21 +15,22 @@ function createAudioService(options) {
|
|
|
15
15
|
descriptor,
|
|
16
16
|
handleMessage(windowId, message, send) {
|
|
17
17
|
if (message.type !== "ifc.emit") return;
|
|
18
|
-
const
|
|
18
|
+
const ifcMessage = message;
|
|
19
|
+
const topic = typeof ifcMessage.topic === "string" ? ifcMessage.topic : void 0;
|
|
19
20
|
if (!topic?.startsWith("audio:")) return;
|
|
20
21
|
const action = topic.slice(6);
|
|
21
|
-
const payload =
|
|
22
|
+
const payload = ifcMessage.payload && typeof ifcMessage.payload === "object" ? ifcMessage.payload : {};
|
|
22
23
|
switch (action) {
|
|
23
24
|
case "register": {
|
|
24
25
|
const nappletClass = typeof payload.nappletClass === "string" ? payload.nappletClass : "";
|
|
25
26
|
const title = typeof payload.title === "string" ? payload.title : "";
|
|
26
27
|
sources.set(windowId, { windowId, nappletClass, title, muted: false });
|
|
27
|
-
|
|
28
|
+
notify2();
|
|
28
29
|
break;
|
|
29
30
|
}
|
|
30
31
|
case "unregister": {
|
|
31
32
|
if (sources.delete(windowId)) {
|
|
32
|
-
|
|
33
|
+
notify2();
|
|
33
34
|
}
|
|
34
35
|
break;
|
|
35
36
|
}
|
|
@@ -39,7 +40,7 @@ function createAudioService(options) {
|
|
|
39
40
|
if (typeof payload.title === "string") {
|
|
40
41
|
source.title = payload.title;
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
notify2();
|
|
43
44
|
break;
|
|
44
45
|
}
|
|
45
46
|
case "mute": {
|
|
@@ -48,7 +49,7 @@ function createAudioService(options) {
|
|
|
48
49
|
const source = sources.get(targetWindowId);
|
|
49
50
|
if (source) {
|
|
50
51
|
source.muted = muted;
|
|
51
|
-
|
|
52
|
+
notify2();
|
|
52
53
|
}
|
|
53
54
|
send({ type: "ifc.event", topic: "napplet:audio-muted", payload: { muted } });
|
|
54
55
|
break;
|
|
@@ -59,7 +60,7 @@ function createAudioService(options) {
|
|
|
59
60
|
},
|
|
60
61
|
onWindowDestroyed(windowId) {
|
|
61
62
|
if (sources.delete(windowId)) {
|
|
62
|
-
|
|
63
|
+
notify2();
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
};
|
|
@@ -73,42 +74,134 @@ function generateId() {
|
|
|
73
74
|
idCounter++;
|
|
74
75
|
return `notif-${Date.now()}-${idCounter}`;
|
|
75
76
|
}
|
|
76
|
-
function
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
function getAllNotifications(store) {
|
|
78
|
+
const all = [];
|
|
79
|
+
for (const windowNotifs of store.notifications.values()) {
|
|
80
|
+
all.push(...windowNotifs);
|
|
81
|
+
}
|
|
82
|
+
return all;
|
|
83
|
+
}
|
|
84
|
+
function notify(store) {
|
|
85
|
+
store.onChange?.(getAllNotifications(store));
|
|
86
|
+
}
|
|
87
|
+
function getWindowNotifications(store, windowId) {
|
|
88
|
+
let list = store.notifications.get(windowId);
|
|
89
|
+
if (!list) {
|
|
90
|
+
list = [];
|
|
91
|
+
store.notifications.set(windowId, list);
|
|
86
92
|
}
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
return list;
|
|
94
|
+
}
|
|
95
|
+
function enforceLimit(store, list) {
|
|
96
|
+
while (list.length > store.maxPerWindow) {
|
|
97
|
+
list.shift();
|
|
89
98
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
}
|
|
100
|
+
function findById(store, id) {
|
|
101
|
+
for (const [windowId, list] of store.notifications) {
|
|
102
|
+
const index = list.findIndex((n) => n.id === id);
|
|
103
|
+
if (index !== -1) {
|
|
104
|
+
return [windowId, list[index], index];
|
|
95
105
|
}
|
|
96
|
-
return list;
|
|
97
106
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
function createNotification(store, windowId, title, body) {
|
|
110
|
+
const notification = {
|
|
111
|
+
id: generateId(),
|
|
112
|
+
windowId,
|
|
113
|
+
title,
|
|
114
|
+
body,
|
|
115
|
+
read: false,
|
|
116
|
+
createdAt: Math.floor(Date.now() / 1e3)
|
|
117
|
+
};
|
|
118
|
+
const list = getWindowNotifications(store, windowId);
|
|
119
|
+
list.push(notification);
|
|
120
|
+
enforceLimit(store, list);
|
|
121
|
+
notify(store);
|
|
122
|
+
return notification;
|
|
123
|
+
}
|
|
124
|
+
function dismissNotification(store, id) {
|
|
125
|
+
const found = findById(store, id);
|
|
126
|
+
if (!found) return;
|
|
127
|
+
const [foundWindowId, , index] = found;
|
|
128
|
+
const list = store.notifications.get(foundWindowId);
|
|
129
|
+
if (!list) return;
|
|
130
|
+
list.splice(index, 1);
|
|
131
|
+
if (list.length === 0) store.notifications.delete(foundWindowId);
|
|
132
|
+
notify(store);
|
|
133
|
+
}
|
|
134
|
+
function markNotificationRead(store, id) {
|
|
135
|
+
const found = findById(store, id);
|
|
136
|
+
if (!found) return;
|
|
137
|
+
const [, notification] = found;
|
|
138
|
+
if (!notification.read) {
|
|
139
|
+
notification.read = true;
|
|
140
|
+
notify(store);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function handleNotifyEnvelope(store, windowId, action, msg, send) {
|
|
144
|
+
switch (action) {
|
|
145
|
+
case "create": {
|
|
146
|
+
const title = typeof msg.title === "string" ? msg.title : "";
|
|
147
|
+
const body = typeof msg.body === "string" ? msg.body : "";
|
|
148
|
+
const notification = createNotification(store, windowId, title, body);
|
|
149
|
+
send({ type: "notify.created", id: notification.id });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "dismiss": {
|
|
153
|
+
const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
|
|
154
|
+
if (notifId) dismissNotification(store, notifId);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "read": {
|
|
158
|
+
const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
|
|
159
|
+
if (notifId) markNotificationRead(store, notifId);
|
|
160
|
+
break;
|
|
101
161
|
}
|
|
162
|
+
case "list": {
|
|
163
|
+
const windowNotifs = store.notifications.get(windowId) ?? [];
|
|
164
|
+
send({ type: "notify.listed", notifications: windowNotifs });
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
break;
|
|
102
169
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
}
|
|
171
|
+
function handleIfcNotification(store, windowId, action, payload, send) {
|
|
172
|
+
switch (action) {
|
|
173
|
+
case "create": {
|
|
174
|
+
const title = typeof payload.title === "string" ? payload.title : "";
|
|
175
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
176
|
+
const notification = createNotification(store, windowId, title, body);
|
|
177
|
+
send({ type: "ifc.event", topic: "notifications:created", payload: { id: notification.id } });
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "dismiss": {
|
|
181
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
182
|
+
if (id) dismissNotification(store, id);
|
|
183
|
+
break;
|
|
109
184
|
}
|
|
110
|
-
|
|
185
|
+
case "read": {
|
|
186
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
187
|
+
if (id) markNotificationRead(store, id);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "list": {
|
|
191
|
+
const windowNotifs = store.notifications.get(windowId) ?? [];
|
|
192
|
+
send({ type: "ifc.event", topic: "notifications:listed", payload: { notifications: windowNotifs } });
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
break;
|
|
111
197
|
}
|
|
198
|
+
}
|
|
199
|
+
function createNotificationService(options) {
|
|
200
|
+
const store = {
|
|
201
|
+
notifications: /* @__PURE__ */ new Map(),
|
|
202
|
+
onChange: options?.onChange,
|
|
203
|
+
maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW
|
|
204
|
+
};
|
|
112
205
|
const descriptor = {
|
|
113
206
|
name: "notifications",
|
|
114
207
|
version: NOTIFICATION_SERVICE_VERSION,
|
|
@@ -119,132 +212,18 @@ function createNotificationService(options) {
|
|
|
119
212
|
handleMessage(windowId, message, send) {
|
|
120
213
|
const msg = message;
|
|
121
214
|
if (message.type.startsWith("notify.")) {
|
|
122
|
-
|
|
123
|
-
switch (action2) {
|
|
124
|
-
case "create": {
|
|
125
|
-
const title = typeof msg.title === "string" ? msg.title : "";
|
|
126
|
-
const body = typeof msg.body === "string" ? msg.body : "";
|
|
127
|
-
const id = generateId();
|
|
128
|
-
const notification = {
|
|
129
|
-
id,
|
|
130
|
-
windowId,
|
|
131
|
-
title,
|
|
132
|
-
body,
|
|
133
|
-
read: false,
|
|
134
|
-
createdAt: Math.floor(Date.now() / 1e3)
|
|
135
|
-
};
|
|
136
|
-
const list = getWindowNotifications(windowId);
|
|
137
|
-
list.push(notification);
|
|
138
|
-
enforceLimit(list);
|
|
139
|
-
notify();
|
|
140
|
-
send({ type: "notify.created", id });
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
case "dismiss": {
|
|
144
|
-
const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
|
|
145
|
-
if (!notifId) return;
|
|
146
|
-
const found = findById(notifId);
|
|
147
|
-
if (found) {
|
|
148
|
-
const [foundWindowId, , index] = found;
|
|
149
|
-
const list = notifications.get(foundWindowId);
|
|
150
|
-
if (list) {
|
|
151
|
-
list.splice(index, 1);
|
|
152
|
-
if (list.length === 0) notifications.delete(foundWindowId);
|
|
153
|
-
notify();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
case "read": {
|
|
159
|
-
const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
|
|
160
|
-
if (!notifId) return;
|
|
161
|
-
const found = findById(notifId);
|
|
162
|
-
if (found) {
|
|
163
|
-
const [, notification] = found;
|
|
164
|
-
if (!notification.read) {
|
|
165
|
-
notification.read = true;
|
|
166
|
-
notify();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
case "list": {
|
|
172
|
-
const windowNotifs = notifications.get(windowId) ?? [];
|
|
173
|
-
send({ type: "notify.listed", notifications: windowNotifs });
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
default:
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
215
|
+
handleNotifyEnvelope(store, windowId, message.type.slice(7), msg, send);
|
|
179
216
|
return;
|
|
180
217
|
}
|
|
181
218
|
if (message.type !== "ifc.emit") return;
|
|
182
219
|
const topic = msg.topic;
|
|
183
220
|
if (!topic?.startsWith("notifications:")) return;
|
|
184
|
-
const action = topic.slice(14);
|
|
185
221
|
const payload = msg.payload ?? {};
|
|
186
|
-
|
|
187
|
-
case "create": {
|
|
188
|
-
const title = typeof payload.title === "string" ? payload.title : "";
|
|
189
|
-
const body = typeof payload.body === "string" ? payload.body : "";
|
|
190
|
-
const id = generateId();
|
|
191
|
-
const notification = {
|
|
192
|
-
id,
|
|
193
|
-
windowId,
|
|
194
|
-
title,
|
|
195
|
-
body,
|
|
196
|
-
read: false,
|
|
197
|
-
createdAt: Math.floor(Date.now() / 1e3)
|
|
198
|
-
};
|
|
199
|
-
const list = getWindowNotifications(windowId);
|
|
200
|
-
list.push(notification);
|
|
201
|
-
enforceLimit(list);
|
|
202
|
-
notify();
|
|
203
|
-
send({ type: "ifc.event", topic: "notifications:created", payload: { id } });
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
case "dismiss": {
|
|
207
|
-
const id = typeof payload.id === "string" ? payload.id : "";
|
|
208
|
-
if (!id) return;
|
|
209
|
-
const found = findById(id);
|
|
210
|
-
if (found) {
|
|
211
|
-
const [foundWindowId, , index] = found;
|
|
212
|
-
const list = notifications.get(foundWindowId);
|
|
213
|
-
if (list) {
|
|
214
|
-
list.splice(index, 1);
|
|
215
|
-
if (list.length === 0) {
|
|
216
|
-
notifications.delete(foundWindowId);
|
|
217
|
-
}
|
|
218
|
-
notify();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
case "read": {
|
|
224
|
-
const id = typeof payload.id === "string" ? payload.id : "";
|
|
225
|
-
if (!id) return;
|
|
226
|
-
const found = findById(id);
|
|
227
|
-
if (found) {
|
|
228
|
-
const [, notification] = found;
|
|
229
|
-
if (!notification.read) {
|
|
230
|
-
notification.read = true;
|
|
231
|
-
notify();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
case "list": {
|
|
237
|
-
const windowNotifs = notifications.get(windowId) ?? [];
|
|
238
|
-
send({ type: "ifc.event", topic: "notifications:listed", payload: { notifications: windowNotifs } });
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
default:
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
222
|
+
handleIfcNotification(store, windowId, topic.slice(14), payload, send);
|
|
244
223
|
},
|
|
245
224
|
onWindowDestroyed(windowId) {
|
|
246
|
-
if (notifications.delete(windowId)) {
|
|
247
|
-
notify();
|
|
225
|
+
if (store.notifications.delete(windowId)) {
|
|
226
|
+
notify(store);
|
|
248
227
|
}
|
|
249
228
|
}
|
|
250
229
|
};
|
|
@@ -252,6 +231,156 @@ function createNotificationService(options) {
|
|
|
252
231
|
|
|
253
232
|
// src/identity-service.ts
|
|
254
233
|
var IDENTITY_SERVICE_VERSION = "1.0.0";
|
|
234
|
+
var DECRYPT_ERROR_CODES = [
|
|
235
|
+
"class-forbidden",
|
|
236
|
+
"signer-denied",
|
|
237
|
+
"signer-unavailable",
|
|
238
|
+
"decrypt-failed",
|
|
239
|
+
"malformed-wrap",
|
|
240
|
+
"impersonation",
|
|
241
|
+
"unsupported-encryption",
|
|
242
|
+
"policy-denied"
|
|
243
|
+
];
|
|
244
|
+
var DECRYPT_ERROR_CODE_SET = new Set(DECRYPT_ERROR_CODES);
|
|
245
|
+
function isDecryptErrorCode(value) {
|
|
246
|
+
return typeof value === "string" && DECRYPT_ERROR_CODE_SET.has(value);
|
|
247
|
+
}
|
|
248
|
+
function normalizeDecryptError(error) {
|
|
249
|
+
if (isDecryptErrorCode(error)) return error;
|
|
250
|
+
if (typeof error === "object" && error !== null) {
|
|
251
|
+
const candidate = error;
|
|
252
|
+
if (isDecryptErrorCode(candidate.code)) return candidate.code;
|
|
253
|
+
if (isDecryptErrorCode(candidate.error)) return candidate.error;
|
|
254
|
+
if (isDecryptErrorCode(candidate.message)) return candidate.message;
|
|
255
|
+
}
|
|
256
|
+
return "decrypt-failed";
|
|
257
|
+
}
|
|
258
|
+
function sendDecryptError(id, error, send) {
|
|
259
|
+
const result = {
|
|
260
|
+
type: "identity.decrypt.error",
|
|
261
|
+
id,
|
|
262
|
+
error
|
|
263
|
+
};
|
|
264
|
+
send(result);
|
|
265
|
+
}
|
|
266
|
+
function isStringArrayArray(value) {
|
|
267
|
+
return Array.isArray(value) && value.every(
|
|
268
|
+
(tag) => Array.isArray(tag) && tag.every((part) => typeof part === "string")
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
function isNostrEvent(value) {
|
|
272
|
+
const event = value;
|
|
273
|
+
return typeof event === "object" && event !== null && typeof event.id === "string" && typeof event.pubkey === "string" && typeof event.created_at === "number" && typeof event.kind === "number" && isStringArrayArray(event.tags) && typeof event.content === "string" && typeof event.sig === "string";
|
|
274
|
+
}
|
|
275
|
+
function isRumor(value) {
|
|
276
|
+
const rumor = value;
|
|
277
|
+
return typeof rumor === "object" && rumor !== null && typeof rumor.id === "string" && typeof rumor.pubkey === "string" && typeof rumor.created_at === "number" && typeof rumor.kind === "number" && isStringArrayArray(rumor.tags) && typeof rumor.content === "string";
|
|
278
|
+
}
|
|
279
|
+
function isGiftWrapDecryptResult(value) {
|
|
280
|
+
const result = value;
|
|
281
|
+
return typeof result === "object" && result !== null && isNostrEvent(result.seal) && isRumor(result.rumor);
|
|
282
|
+
}
|
|
283
|
+
function firstDecodedByte(content) {
|
|
284
|
+
const trimmed = content.trim();
|
|
285
|
+
if (trimmed.length === 0) return null;
|
|
286
|
+
const normalized = trimmed.replace(/-/g, "+").replace(/_/g, "/");
|
|
287
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
288
|
+
try {
|
|
289
|
+
const decoded = atob(padded);
|
|
290
|
+
return decoded.length > 0 ? decoded.charCodeAt(0) : null;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function detectEncryptionMode(event) {
|
|
296
|
+
if (event.kind === 4) return "nip04";
|
|
297
|
+
if (event.kind === 1059) return "nip17";
|
|
298
|
+
if (event.kind === 14 || firstDecodedByte(event.content) === 2) {
|
|
299
|
+
return "nip44-direct";
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function rumorFromSignedEvent(event, content) {
|
|
304
|
+
return {
|
|
305
|
+
id: event.id,
|
|
306
|
+
pubkey: event.pubkey,
|
|
307
|
+
kind: event.kind,
|
|
308
|
+
tags: event.tags,
|
|
309
|
+
created_at: event.created_at,
|
|
310
|
+
content
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async function handleDecrypt(id, message, send, options) {
|
|
314
|
+
const event = message.event;
|
|
315
|
+
if (!isNostrEvent(event)) {
|
|
316
|
+
sendDecryptError(id, "malformed-wrap", send);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const verifyEvent = options.verifyEvent ?? (() => true);
|
|
320
|
+
let verified;
|
|
321
|
+
try {
|
|
322
|
+
verified = await Promise.resolve(verifyEvent(event));
|
|
323
|
+
} catch {
|
|
324
|
+
sendDecryptError(id, "malformed-wrap", send);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!verified) {
|
|
328
|
+
sendDecryptError(id, "malformed-wrap", send);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const mode = detectEncryptionMode(event);
|
|
332
|
+
if (!mode) {
|
|
333
|
+
sendDecryptError(id, "unsupported-encryption", send);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const decryptor = options.getDecryptor?.() ?? null;
|
|
337
|
+
if (!decryptor) {
|
|
338
|
+
sendDecryptError(id, "signer-unavailable", send);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
if (mode === "nip04") {
|
|
343
|
+
const plaintext = await decryptor.nip04Decrypt(event.pubkey, event.content);
|
|
344
|
+
const result2 = {
|
|
345
|
+
type: "identity.decrypt.result",
|
|
346
|
+
id,
|
|
347
|
+
rumor: rumorFromSignedEvent(event, plaintext),
|
|
348
|
+
sender: event.pubkey
|
|
349
|
+
};
|
|
350
|
+
send(result2);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (mode === "nip44-direct") {
|
|
354
|
+
const plaintext = await decryptor.nip44Decrypt(event.pubkey, event.content);
|
|
355
|
+
const result2 = {
|
|
356
|
+
type: "identity.decrypt.result",
|
|
357
|
+
id,
|
|
358
|
+
rumor: rumorFromSignedEvent(event, plaintext),
|
|
359
|
+
sender: event.pubkey
|
|
360
|
+
};
|
|
361
|
+
send(result2);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const unwrapped = await decryptor.unwrapGiftWrap(event);
|
|
365
|
+
if (!isGiftWrapDecryptResult(unwrapped)) {
|
|
366
|
+
sendDecryptError(id, "malformed-wrap", send);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (unwrapped.seal.pubkey !== unwrapped.rumor.pubkey) {
|
|
370
|
+
sendDecryptError(id, "impersonation", send);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const result = {
|
|
374
|
+
type: "identity.decrypt.result",
|
|
375
|
+
id,
|
|
376
|
+
rumor: unwrapped.rumor,
|
|
377
|
+
sender: unwrapped.seal.pubkey
|
|
378
|
+
};
|
|
379
|
+
send(result);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
sendDecryptError(id, normalizeDecryptError(error), send);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
255
384
|
function createIdentityService(options) {
|
|
256
385
|
return {
|
|
257
386
|
descriptor: {
|
|
@@ -264,6 +393,9 @@ function createIdentityService(options) {
|
|
|
264
393
|
function sendError(typeBase, error) {
|
|
265
394
|
send({ type: `${typeBase}.error`, id, error });
|
|
266
395
|
}
|
|
396
|
+
function sendSignerError(typeBase, fallback, err) {
|
|
397
|
+
sendError(typeBase, err?.message ?? fallback);
|
|
398
|
+
}
|
|
267
399
|
const signer = options.getSigner();
|
|
268
400
|
switch (message.type) {
|
|
269
401
|
case "identity.getPublicKey": {
|
|
@@ -283,12 +415,7 @@ function createIdentityService(options) {
|
|
|
283
415
|
pubkey: pubkey ?? ""
|
|
284
416
|
};
|
|
285
417
|
send(result);
|
|
286
|
-
}).catch((err) =>
|
|
287
|
-
sendError(
|
|
288
|
-
"identity.getPublicKey",
|
|
289
|
-
err?.message ?? "getPublicKey failed"
|
|
290
|
-
);
|
|
291
|
-
});
|
|
418
|
+
}).catch((err) => sendSignerError("identity.getPublicKey", "getPublicKey failed", err));
|
|
292
419
|
return;
|
|
293
420
|
}
|
|
294
421
|
case "identity.getRelays": {
|
|
@@ -303,12 +430,7 @@ function createIdentityService(options) {
|
|
|
303
430
|
relays
|
|
304
431
|
};
|
|
305
432
|
send(result);
|
|
306
|
-
}).catch((err) =>
|
|
307
|
-
sendError(
|
|
308
|
-
"identity.getRelays",
|
|
309
|
-
err?.message ?? "getRelays failed"
|
|
310
|
-
);
|
|
311
|
-
});
|
|
433
|
+
}).catch((err) => sendSignerError("identity.getRelays", "getRelays failed", err));
|
|
312
434
|
return;
|
|
313
435
|
}
|
|
314
436
|
case "identity.getProfile": {
|
|
@@ -374,6 +496,10 @@ function createIdentityService(options) {
|
|
|
374
496
|
send(result);
|
|
375
497
|
return;
|
|
376
498
|
}
|
|
499
|
+
case "identity.decrypt": {
|
|
500
|
+
void handleDecrypt(id, message, send, options);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
377
503
|
default:
|
|
378
504
|
sendError(message.type, `Unknown identity method: ${message.type}`);
|
|
379
505
|
}
|
|
@@ -395,10 +521,11 @@ function createRelayPoolService(options) {
|
|
|
395
521
|
description: "Relay pool subscription and publishing"
|
|
396
522
|
},
|
|
397
523
|
handleMessage(windowId, message, send) {
|
|
524
|
+
const relayMessage = message;
|
|
398
525
|
if (message.type === "relay.subscribe") {
|
|
399
|
-
const subId =
|
|
526
|
+
const subId = relayMessage.subId;
|
|
400
527
|
if (typeof subId !== "string") return;
|
|
401
|
-
const filters =
|
|
528
|
+
const filters = Array.isArray(relayMessage.filters) ? relayMessage.filters : [];
|
|
402
529
|
const subKey = `${windowId}:${subId}`;
|
|
403
530
|
const existing = tracked.get(subKey);
|
|
404
531
|
if (existing) {
|
|
@@ -433,7 +560,7 @@ function createRelayPoolService(options) {
|
|
|
433
560
|
return;
|
|
434
561
|
}
|
|
435
562
|
if (message.type === "relay.close") {
|
|
436
|
-
const subId =
|
|
563
|
+
const subId = relayMessage.subId;
|
|
437
564
|
if (typeof subId !== "string") return;
|
|
438
565
|
const subKey = `${windowId}:${subId}`;
|
|
439
566
|
const entry = tracked.get(subKey);
|
|
@@ -445,14 +572,14 @@ function createRelayPoolService(options) {
|
|
|
445
572
|
return;
|
|
446
573
|
}
|
|
447
574
|
if (message.type === "relay.publish") {
|
|
448
|
-
const event =
|
|
575
|
+
const event = relayMessage.event;
|
|
449
576
|
if (event && typeof event === "object" && options.isAvailable()) {
|
|
450
577
|
options.publish(event);
|
|
451
578
|
}
|
|
452
579
|
return;
|
|
453
580
|
}
|
|
454
581
|
if (message.type === "relay.publishEncrypted") {
|
|
455
|
-
const event =
|
|
582
|
+
const event = relayMessage.event;
|
|
456
583
|
if (event && typeof event === "object" && options.isAvailable()) {
|
|
457
584
|
options.publish(event);
|
|
458
585
|
}
|
|
@@ -481,10 +608,11 @@ function createCacheService(options) {
|
|
|
481
608
|
description: "Local event cache (IndexedDB, worker relay, etc.)"
|
|
482
609
|
},
|
|
483
610
|
handleMessage(_windowId, message, send) {
|
|
611
|
+
const relayMessage = message;
|
|
484
612
|
if (message.type === "relay.subscribe") {
|
|
485
|
-
const subId =
|
|
613
|
+
const subId = relayMessage.subId;
|
|
486
614
|
if (typeof subId !== "string") return;
|
|
487
|
-
const filters =
|
|
615
|
+
const filters = Array.isArray(relayMessage.filters) ? relayMessage.filters : [];
|
|
488
616
|
if (!options.isAvailable()) {
|
|
489
617
|
send({ type: "relay.eose", subId });
|
|
490
618
|
return;
|
|
@@ -500,7 +628,7 @@ function createCacheService(options) {
|
|
|
500
628
|
return;
|
|
501
629
|
}
|
|
502
630
|
if (message.type === "relay.publish") {
|
|
503
|
-
const event =
|
|
631
|
+
const event = relayMessage.event;
|
|
504
632
|
if (event && typeof event === "object" && options.isAvailable()) {
|
|
505
633
|
try {
|
|
506
634
|
options.store(event);
|
|
@@ -537,6 +665,7 @@ function createCoordinatedRelay(options) {
|
|
|
537
665
|
description: "Coordinated relay pool + cache with dedup and unified EOSE"
|
|
538
666
|
},
|
|
539
667
|
handleMessage(windowId, message, send) {
|
|
668
|
+
const relayMessage = message;
|
|
540
669
|
if (message.type === "relay.subscribe") {
|
|
541
670
|
let deliver2 = function(event) {
|
|
542
671
|
if (tracked.seenIds.has(event.id)) return;
|
|
@@ -544,9 +673,9 @@ function createCoordinatedRelay(options) {
|
|
|
544
673
|
if (subs.has(subKey)) send({ type: "relay.event", subId, event });
|
|
545
674
|
};
|
|
546
675
|
var deliver = deliver2;
|
|
547
|
-
const subId =
|
|
676
|
+
const subId = relayMessage.subId;
|
|
548
677
|
if (typeof subId !== "string") return;
|
|
549
|
-
const filters =
|
|
678
|
+
const filters = Array.isArray(relayMessage.filters) ? relayMessage.filters : [];
|
|
550
679
|
const subKey = `${windowId}:${subId}`;
|
|
551
680
|
const existing = subs.get(subKey);
|
|
552
681
|
if (existing) {
|
|
@@ -608,7 +737,7 @@ function createCoordinatedRelay(options) {
|
|
|
608
737
|
return;
|
|
609
738
|
}
|
|
610
739
|
if (message.type === "relay.close") {
|
|
611
|
-
const subId =
|
|
740
|
+
const subId = relayMessage.subId;
|
|
612
741
|
if (typeof subId !== "string") return;
|
|
613
742
|
const subKey = `${windowId}:${subId}`;
|
|
614
743
|
const entry = subs.get(subKey);
|
|
@@ -620,7 +749,7 @@ function createCoordinatedRelay(options) {
|
|
|
620
749
|
return;
|
|
621
750
|
}
|
|
622
751
|
if (message.type === "relay.publish") {
|
|
623
|
-
const event =
|
|
752
|
+
const event = relayMessage.event;
|
|
624
753
|
if (!event || typeof event !== "object") return;
|
|
625
754
|
if (options.relayPool.isAvailable()) {
|
|
626
755
|
options.relayPool.publish(event);
|
|
@@ -648,31 +777,266 @@ function createCoordinatedRelay(options) {
|
|
|
648
777
|
}
|
|
649
778
|
|
|
650
779
|
// src/keys-service.ts
|
|
651
|
-
var KEYS_SERVICE_VERSION = "1.
|
|
780
|
+
var KEYS_SERVICE_VERSION = "1.2.0";
|
|
781
|
+
var MODIFIER_ALIASES = {
|
|
782
|
+
ctrl: "ctrl",
|
|
783
|
+
control: "ctrl",
|
|
784
|
+
alt: "alt",
|
|
785
|
+
option: "alt",
|
|
786
|
+
shift: "shift",
|
|
787
|
+
meta: "meta",
|
|
788
|
+
cmd: "meta",
|
|
789
|
+
command: "meta",
|
|
790
|
+
win: "meta",
|
|
791
|
+
super: "meta"
|
|
792
|
+
};
|
|
793
|
+
function parseChord(chord) {
|
|
794
|
+
if (chord.length === 0) throw new Error("empty chord");
|
|
795
|
+
const parts = chord.split("+");
|
|
796
|
+
const out = { ctrl: false, alt: false, shift: false, meta: false, key: "" };
|
|
797
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
798
|
+
const tok = parts[i].trim().toLowerCase();
|
|
799
|
+
if (tok.length === 0) continue;
|
|
800
|
+
const slot = MODIFIER_ALIASES[tok];
|
|
801
|
+
if (!slot) throw new Error(`unknown modifier: ${parts[i]}`);
|
|
802
|
+
out[slot] = true;
|
|
803
|
+
}
|
|
804
|
+
const keyTok = parts[parts.length - 1].trim();
|
|
805
|
+
if (keyTok.length === 0) throw new Error(`empty key in chord: ${chord}`);
|
|
806
|
+
out.key = keyTok.length === 1 ? keyTok.toUpperCase() : keyTok;
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
652
809
|
function createKeysService(options = {}) {
|
|
653
810
|
const descriptor = {
|
|
654
811
|
name: "keys",
|
|
655
812
|
version: KEYS_SERVICE_VERSION,
|
|
656
|
-
description: "NIP-5D keys NUB reference handler (
|
|
813
|
+
description: options.hostBridge ? "NIP-5D keys NUB reference handler (host-bridge delegated)" : "NIP-5D keys NUB reference handler (document-level chord listener)"
|
|
814
|
+
};
|
|
815
|
+
function chordSpecKey(spec) {
|
|
816
|
+
return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;
|
|
817
|
+
}
|
|
818
|
+
const reservedChordKeys = /* @__PURE__ */ new Set();
|
|
819
|
+
if (options.reservedChords) {
|
|
820
|
+
for (const chordStr of options.reservedChords) {
|
|
821
|
+
reservedChordKeys.add(chordSpecKey(parseChord(chordStr)));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function forwardKey(m) {
|
|
825
|
+
const k = m.key.length === 1 ? m.key.toUpperCase() : m.key;
|
|
826
|
+
return `${m.ctrl}|${m.alt}|${m.shift}|${m.meta}|${k}`;
|
|
827
|
+
}
|
|
828
|
+
function forwardPayload(m) {
|
|
829
|
+
return {
|
|
830
|
+
key: m.key,
|
|
831
|
+
code: m.code,
|
|
832
|
+
ctrlKey: m.ctrl,
|
|
833
|
+
altKey: m.alt,
|
|
834
|
+
shiftKey: m.shift,
|
|
835
|
+
metaKey: m.meta
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function eventKey(ev) {
|
|
839
|
+
const k = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;
|
|
840
|
+
return `${ev.ctrlKey}|${ev.altKey}|${ev.shiftKey}|${ev.metaKey}|${k}`;
|
|
841
|
+
}
|
|
842
|
+
if (options.hostBridge) {
|
|
843
|
+
const bridge = options.hostBridge;
|
|
844
|
+
const bridgeWindowActions = /* @__PURE__ */ new Map();
|
|
845
|
+
const unsubscribeHandles = /* @__PURE__ */ new Map();
|
|
846
|
+
return {
|
|
847
|
+
descriptor,
|
|
848
|
+
handleMessage(windowId, message, send) {
|
|
849
|
+
switch (message.type) {
|
|
850
|
+
case "keys.forward": {
|
|
851
|
+
const m = message;
|
|
852
|
+
const reserved = reservedChordKeys.has(forwardKey(m));
|
|
853
|
+
options.onForward?.(forwardPayload(m));
|
|
854
|
+
if (reserved) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
case "keys.registerAction": {
|
|
860
|
+
const m = message;
|
|
861
|
+
if (m.action.defaultKey) {
|
|
862
|
+
try {
|
|
863
|
+
const unsubscribe = bridge.subscribe(m.action.defaultKey, (ev) => {
|
|
864
|
+
const e = ev;
|
|
865
|
+
if ("repeat" in e && e.repeat) return;
|
|
866
|
+
options.onForward?.({
|
|
867
|
+
key: e.key,
|
|
868
|
+
code: e.code,
|
|
869
|
+
ctrlKey: e.ctrlKey,
|
|
870
|
+
altKey: e.altKey,
|
|
871
|
+
shiftKey: e.shiftKey,
|
|
872
|
+
metaKey: e.metaKey
|
|
873
|
+
});
|
|
874
|
+
const payload = {
|
|
875
|
+
type: "keys.action",
|
|
876
|
+
actionId: m.action.id
|
|
877
|
+
};
|
|
878
|
+
send(payload);
|
|
879
|
+
});
|
|
880
|
+
unsubscribeHandles.set(m.action.id, unsubscribe);
|
|
881
|
+
if (!bridgeWindowActions.has(windowId)) bridgeWindowActions.set(windowId, /* @__PURE__ */ new Set());
|
|
882
|
+
bridgeWindowActions.get(windowId).add(m.action.id);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
const id = m.id ?? "";
|
|
885
|
+
send({
|
|
886
|
+
type: "keys.registerAction.error",
|
|
887
|
+
id,
|
|
888
|
+
error: `bridge subscribe failed: ${err.message}`
|
|
889
|
+
});
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const result = {
|
|
894
|
+
type: "keys.registerAction.result",
|
|
895
|
+
id: m.id,
|
|
896
|
+
actionId: m.action.id,
|
|
897
|
+
...m.action.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
898
|
+
};
|
|
899
|
+
send(result);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
case "keys.unregisterAction": {
|
|
903
|
+
const m = message;
|
|
904
|
+
if (m.actionId) {
|
|
905
|
+
const unsubscribe = unsubscribeHandles.get(m.actionId);
|
|
906
|
+
if (unsubscribe) {
|
|
907
|
+
try {
|
|
908
|
+
unsubscribe();
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
unsubscribeHandles.delete(m.actionId);
|
|
912
|
+
for (const [wid, set] of bridgeWindowActions.entries()) {
|
|
913
|
+
if (set.delete(m.actionId) && set.size === 0) bridgeWindowActions.delete(wid);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
default: {
|
|
920
|
+
const id = message.id ?? "";
|
|
921
|
+
send({
|
|
922
|
+
type: `${message.type}.error`,
|
|
923
|
+
id,
|
|
924
|
+
error: `Unknown keys method: ${message.type}`
|
|
925
|
+
});
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
onWindowDestroyed(windowId) {
|
|
931
|
+
const actions = bridgeWindowActions.get(windowId);
|
|
932
|
+
if (!actions) return;
|
|
933
|
+
for (const actionId of actions) {
|
|
934
|
+
const unsubscribe = unsubscribeHandles.get(actionId);
|
|
935
|
+
if (unsubscribe) {
|
|
936
|
+
try {
|
|
937
|
+
unsubscribe();
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
unsubscribeHandles.delete(actionId);
|
|
942
|
+
}
|
|
943
|
+
bridgeWindowActions.delete(windowId);
|
|
944
|
+
},
|
|
945
|
+
destroy() {
|
|
946
|
+
for (const unsubscribe of unsubscribeHandles.values()) {
|
|
947
|
+
try {
|
|
948
|
+
unsubscribe();
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
unsubscribeHandles.clear();
|
|
953
|
+
bridgeWindowActions.clear();
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const actionRegistry = /* @__PURE__ */ new Map();
|
|
958
|
+
const windowActions = /* @__PURE__ */ new Map();
|
|
959
|
+
const sendHandles = /* @__PURE__ */ new Map();
|
|
960
|
+
const target = options.listenerTarget ?? (typeof document !== "undefined" ? document : new EventTarget());
|
|
961
|
+
function chordMatches(spec, ev) {
|
|
962
|
+
if (spec.ctrl !== ev.ctrlKey) return false;
|
|
963
|
+
if (spec.alt !== ev.altKey) return false;
|
|
964
|
+
if (spec.shift !== ev.shiftKey) return false;
|
|
965
|
+
if (spec.meta !== ev.metaKey) return false;
|
|
966
|
+
const evKey = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;
|
|
967
|
+
return spec.key === evKey;
|
|
968
|
+
}
|
|
969
|
+
const listener = (rawEv) => {
|
|
970
|
+
const ev = rawEv;
|
|
971
|
+
if (ev.repeat) return;
|
|
972
|
+
const isReserved = reservedChordKeys.has(eventKey(ev));
|
|
973
|
+
let anyMatch = false;
|
|
974
|
+
for (const entry of actionRegistry.values()) {
|
|
975
|
+
if (chordMatches(entry.chord, ev)) {
|
|
976
|
+
anyMatch = true;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (isReserved || anyMatch) {
|
|
981
|
+
options.onForward?.({
|
|
982
|
+
key: ev.key,
|
|
983
|
+
code: ev.code,
|
|
984
|
+
ctrlKey: ev.ctrlKey,
|
|
985
|
+
altKey: ev.altKey,
|
|
986
|
+
shiftKey: ev.shiftKey,
|
|
987
|
+
metaKey: ev.metaKey
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
if (isReserved) return;
|
|
991
|
+
for (const [actionId, entry] of actionRegistry.entries()) {
|
|
992
|
+
if (chordMatches(entry.chord, ev)) {
|
|
993
|
+
const send = sendHandles.get(entry.windowId);
|
|
994
|
+
if (send) {
|
|
995
|
+
const payload = {
|
|
996
|
+
type: "keys.action",
|
|
997
|
+
actionId,
|
|
998
|
+
chord: entry.chord
|
|
999
|
+
};
|
|
1000
|
+
send(payload);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
657
1004
|
};
|
|
1005
|
+
target.addEventListener("keydown", listener);
|
|
658
1006
|
return {
|
|
659
1007
|
descriptor,
|
|
660
|
-
handleMessage(
|
|
1008
|
+
handleMessage(windowId, message, send) {
|
|
661
1009
|
switch (message.type) {
|
|
662
1010
|
case "keys.forward": {
|
|
663
1011
|
const m = message;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
ctrlKey: m.ctrl,
|
|
668
|
-
altKey: m.alt,
|
|
669
|
-
shiftKey: m.shift,
|
|
670
|
-
metaKey: m.meta
|
|
671
|
-
});
|
|
1012
|
+
const reserved = reservedChordKeys.has(forwardKey(m));
|
|
1013
|
+
options.onForward?.(forwardPayload(m));
|
|
1014
|
+
if (reserved) return;
|
|
672
1015
|
return;
|
|
673
1016
|
}
|
|
674
1017
|
case "keys.registerAction": {
|
|
675
1018
|
const m = message;
|
|
1019
|
+
sendHandles.set(windowId, send);
|
|
1020
|
+
if (m.action.defaultKey) {
|
|
1021
|
+
try {
|
|
1022
|
+
const chord = parseChord(m.action.defaultKey);
|
|
1023
|
+
actionRegistry.set(m.action.id, {
|
|
1024
|
+
chord,
|
|
1025
|
+
chordString: m.action.defaultKey,
|
|
1026
|
+
windowId
|
|
1027
|
+
});
|
|
1028
|
+
if (!windowActions.has(windowId)) windowActions.set(windowId, /* @__PURE__ */ new Set());
|
|
1029
|
+
windowActions.get(windowId).add(m.action.id);
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
const id = m.id ?? "";
|
|
1032
|
+
send({
|
|
1033
|
+
type: "keys.registerAction.error",
|
|
1034
|
+
id,
|
|
1035
|
+
error: `invalid chord: ${err.message}`
|
|
1036
|
+
});
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
676
1040
|
const result = {
|
|
677
1041
|
type: "keys.registerAction.result",
|
|
678
1042
|
id: m.id,
|
|
@@ -683,6 +1047,19 @@ function createKeysService(options = {}) {
|
|
|
683
1047
|
return;
|
|
684
1048
|
}
|
|
685
1049
|
case "keys.unregisterAction": {
|
|
1050
|
+
const m = message;
|
|
1051
|
+
if (m.actionId && actionRegistry.has(m.actionId)) {
|
|
1052
|
+
const entry = actionRegistry.get(m.actionId);
|
|
1053
|
+
actionRegistry.delete(m.actionId);
|
|
1054
|
+
const set = windowActions.get(entry.windowId);
|
|
1055
|
+
if (set) {
|
|
1056
|
+
set.delete(m.actionId);
|
|
1057
|
+
if (set.size === 0) {
|
|
1058
|
+
windowActions.delete(entry.windowId);
|
|
1059
|
+
sendHandles.delete(entry.windowId);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
686
1063
|
return;
|
|
687
1064
|
}
|
|
688
1065
|
default: {
|
|
@@ -696,66 +1073,382 @@ function createKeysService(options = {}) {
|
|
|
696
1073
|
}
|
|
697
1074
|
}
|
|
698
1075
|
},
|
|
699
|
-
onWindowDestroyed(
|
|
1076
|
+
onWindowDestroyed(windowId) {
|
|
1077
|
+
const actions = windowActions.get(windowId);
|
|
1078
|
+
if (actions) {
|
|
1079
|
+
for (const actionId of actions) actionRegistry.delete(actionId);
|
|
1080
|
+
windowActions.delete(windowId);
|
|
1081
|
+
}
|
|
1082
|
+
sendHandles.delete(windowId);
|
|
1083
|
+
},
|
|
1084
|
+
destroy() {
|
|
1085
|
+
target.removeEventListener("keydown", listener);
|
|
1086
|
+
actionRegistry.clear();
|
|
1087
|
+
windowActions.clear();
|
|
1088
|
+
sendHandles.clear();
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/browser-media-bridge.ts
|
|
1094
|
+
var SILENT_AUDIO_DATA_URL = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=";
|
|
1095
|
+
var DEFAULT_MEDIA_ACTIONS = ["play", "pause", "next", "prev", "seek"];
|
|
1096
|
+
var ACTION_MATRIX = [
|
|
1097
|
+
["play", "play"],
|
|
1098
|
+
["pause", "pause"],
|
|
1099
|
+
["nexttrack", "next"],
|
|
1100
|
+
["previoustrack", "prev"],
|
|
1101
|
+
["seekto", "seek"]
|
|
1102
|
+
];
|
|
1103
|
+
function createBrowserMediaBridge(opts = {}) {
|
|
1104
|
+
const ms = opts.mediaSessionTarget ?? (typeof navigator !== "undefined" && "mediaSession" in navigator ? navigator.mediaSession : null);
|
|
1105
|
+
const doc = opts.documentTarget !== void 0 ? opts.documentTarget : typeof document !== "undefined" ? document : null;
|
|
1106
|
+
let silentAudioEl = null;
|
|
1107
|
+
let activeSessionId = null;
|
|
1108
|
+
let sessionsActive = 0;
|
|
1109
|
+
const actionCallbacks = /* @__PURE__ */ new Set();
|
|
1110
|
+
function primeSilentAudio() {
|
|
1111
|
+
if (silentAudioEl || !doc) return;
|
|
1112
|
+
const el = doc.createElement("audio");
|
|
1113
|
+
el.src = SILENT_AUDIO_DATA_URL;
|
|
1114
|
+
el.loop = true;
|
|
1115
|
+
el.style.display = "none";
|
|
1116
|
+
el.setAttribute("data-kehto-silent-audio-prime", "true");
|
|
1117
|
+
doc.body.appendChild(el);
|
|
1118
|
+
void el.play().catch(() => {
|
|
1119
|
+
});
|
|
1120
|
+
silentAudioEl = el;
|
|
1121
|
+
}
|
|
1122
|
+
function teardownSilentAudio() {
|
|
1123
|
+
if (!silentAudioEl) return;
|
|
1124
|
+
try {
|
|
1125
|
+
silentAudioEl.pause();
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
silentAudioEl.remove();
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
silentAudioEl = null;
|
|
1133
|
+
}
|
|
1134
|
+
function applyActionHandlers(actions = DEFAULT_MEDIA_ACTIONS) {
|
|
1135
|
+
if (!ms) return;
|
|
1136
|
+
for (const [domAction, nubAction] of ACTION_MATRIX) {
|
|
1137
|
+
if (!actions.includes(nubAction)) {
|
|
1138
|
+
try {
|
|
1139
|
+
ms.setActionHandler(domAction, null);
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
ms.setActionHandler(domAction, (details) => {
|
|
1145
|
+
if (!activeSessionId) return;
|
|
1146
|
+
const value = nubAction === "seek" && typeof details?.seekTime === "number" ? details.seekTime : void 0;
|
|
1147
|
+
for (const cb of actionCallbacks) {
|
|
1148
|
+
cb(activeSessionId, nubAction, value);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function writeMetadata(metadata) {
|
|
1154
|
+
if (!ms) return;
|
|
1155
|
+
if (!metadata) {
|
|
1156
|
+
ms.metadata = null;
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const artwork = metadata.artwork?.url ? [{ src: metadata.artwork.url }] : void 0;
|
|
1160
|
+
const init = {
|
|
1161
|
+
title: metadata.title ?? "",
|
|
1162
|
+
artist: metadata.artist ?? "",
|
|
1163
|
+
album: metadata.album ?? "",
|
|
1164
|
+
...artwork ? { artwork } : {}
|
|
1165
|
+
};
|
|
1166
|
+
try {
|
|
1167
|
+
const ctor = globalThis.MediaMetadata;
|
|
1168
|
+
ms.metadata = ctor ? new ctor(init) : init;
|
|
1169
|
+
} catch {
|
|
1170
|
+
ms.metadata = init;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
setMetadata(sessionId, metadata) {
|
|
1175
|
+
if (sessionId === activeSessionId) writeMetadata(metadata);
|
|
1176
|
+
},
|
|
1177
|
+
setPlaybackState(sessionId, state) {
|
|
1178
|
+
if (!ms || sessionId !== activeSessionId) return;
|
|
1179
|
+
ms.playbackState = state === "playing" ? "playing" : state === "paused" || state === "buffering" ? "paused" : "none";
|
|
1180
|
+
},
|
|
1181
|
+
onAction(callback) {
|
|
1182
|
+
actionCallbacks.add(callback);
|
|
1183
|
+
return () => {
|
|
1184
|
+
actionCallbacks.delete(callback);
|
|
1185
|
+
};
|
|
1186
|
+
},
|
|
1187
|
+
setActiveSession(sessionId, actions) {
|
|
1188
|
+
activeSessionId = sessionId;
|
|
1189
|
+
if (!sessionId) {
|
|
1190
|
+
if (ms) {
|
|
1191
|
+
ms.metadata = null;
|
|
1192
|
+
ms.playbackState = "none";
|
|
1193
|
+
for (const [domAction] of ACTION_MATRIX) {
|
|
1194
|
+
try {
|
|
1195
|
+
ms.setActionHandler(domAction, null);
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (sessionsActive === 0) {
|
|
1203
|
+
primeSilentAudio();
|
|
1204
|
+
sessionsActive = 1;
|
|
1205
|
+
}
|
|
1206
|
+
applyActionHandlers(actions ?? DEFAULT_MEDIA_ACTIONS);
|
|
1207
|
+
},
|
|
1208
|
+
destroySession(_sessionId) {
|
|
1209
|
+
sessionsActive = Math.max(0, sessionsActive - 1);
|
|
1210
|
+
if (sessionsActive === 0) teardownSilentAudio();
|
|
700
1211
|
}
|
|
701
1212
|
};
|
|
702
1213
|
}
|
|
703
1214
|
|
|
704
1215
|
// src/media-service.ts
|
|
705
|
-
var MEDIA_SERVICE_VERSION = "1.
|
|
1216
|
+
var MEDIA_SERVICE_VERSION = "1.1.0";
|
|
1217
|
+
function createMediaServiceState(options, bridge) {
|
|
1218
|
+
return {
|
|
1219
|
+
bridge,
|
|
1220
|
+
options,
|
|
1221
|
+
sessionRegistry: /* @__PURE__ */ new Map(),
|
|
1222
|
+
windowSessions: /* @__PURE__ */ new Map(),
|
|
1223
|
+
sendHandles: /* @__PURE__ */ new Map(),
|
|
1224
|
+
activeSessionId: null,
|
|
1225
|
+
touchCounter: 0,
|
|
1226
|
+
sessionCounter: 0
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
function setActive(state, sessionId, actions) {
|
|
1230
|
+
state.activeSessionId = sessionId;
|
|
1231
|
+
state.bridge.setActiveSession?.(sessionId, actions);
|
|
1232
|
+
if (!sessionId) return;
|
|
1233
|
+
const entry = state.sessionRegistry.get(sessionId);
|
|
1234
|
+
if (!entry) return;
|
|
1235
|
+
if (entry.metadata) state.bridge.setMetadata(sessionId, entry.metadata);
|
|
1236
|
+
if (entry.state) state.bridge.setPlaybackState(sessionId, entry.state.status);
|
|
1237
|
+
}
|
|
1238
|
+
function promoteNextActiveOrClear(state) {
|
|
1239
|
+
if (state.sessionRegistry.size === 0) {
|
|
1240
|
+
setActive(state, null);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
let latest = null;
|
|
1244
|
+
for (const entry of state.sessionRegistry.values()) {
|
|
1245
|
+
if (!latest || entry.lastTouched > latest.lastTouched) latest = entry;
|
|
1246
|
+
}
|
|
1247
|
+
setActive(state, latest ? latest.sessionId : null, latest?.actions);
|
|
1248
|
+
}
|
|
1249
|
+
function sendMediaCommand(state, sessionId, action, value) {
|
|
1250
|
+
const entry = state.sessionRegistry.get(sessionId);
|
|
1251
|
+
if (!entry) return;
|
|
1252
|
+
const send = state.sendHandles.get(entry.windowId);
|
|
1253
|
+
if (!send) return;
|
|
1254
|
+
const payload = {
|
|
1255
|
+
type: "media.command",
|
|
1256
|
+
sessionId,
|
|
1257
|
+
action,
|
|
1258
|
+
...typeof value === "number" ? { value } : {}
|
|
1259
|
+
};
|
|
1260
|
+
send(payload);
|
|
1261
|
+
}
|
|
1262
|
+
function registerWindowSession(state, windowId, sessionId) {
|
|
1263
|
+
if (!state.windowSessions.has(windowId)) state.windowSessions.set(windowId, /* @__PURE__ */ new Set());
|
|
1264
|
+
state.windowSessions.get(windowId).add(sessionId);
|
|
1265
|
+
}
|
|
1266
|
+
function sendSessionCreateResult(send, id, fields) {
|
|
1267
|
+
send({
|
|
1268
|
+
type: "media.session.create.result",
|
|
1269
|
+
id: id ?? "",
|
|
1270
|
+
...fields
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
function isMediaPlaybackOwner(value) {
|
|
1274
|
+
return value === "shell" || value === "napplet";
|
|
1275
|
+
}
|
|
1276
|
+
function hasSourceRef(source) {
|
|
1277
|
+
if (!source) return false;
|
|
1278
|
+
if (typeof source.url === "string" && source.url.length > 0) return true;
|
|
1279
|
+
if (typeof source.blossomHash === "string" && source.blossomHash.length > 0) return true;
|
|
1280
|
+
if (source.nostr) {
|
|
1281
|
+
return Boolean(source.nostr.eventId || source.nostr.address);
|
|
1282
|
+
}
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
function canonicalizeSessionId(state, windowId, preferredSessionId) {
|
|
1286
|
+
const trimmed = typeof preferredSessionId === "string" ? preferredSessionId.trim() : "";
|
|
1287
|
+
const hint = trimmed || `session-${++state.sessionCounter}`;
|
|
1288
|
+
if (!state.sessionRegistry.has(hint)) return hint;
|
|
1289
|
+
let next;
|
|
1290
|
+
do {
|
|
1291
|
+
next = `${windowId}:${hint}:${++state.sessionCounter}`;
|
|
1292
|
+
} while (state.sessionRegistry.has(next));
|
|
1293
|
+
return next;
|
|
1294
|
+
}
|
|
1295
|
+
function handleSessionCreate(state, windowId, message, send) {
|
|
1296
|
+
if (!isMediaPlaybackOwner(message.owner)) {
|
|
1297
|
+
sendSessionCreateResult(send, message.id, { error: "missing owner" });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (message.owner === "shell") {
|
|
1301
|
+
if (!hasSourceRef(message.source)) {
|
|
1302
|
+
sendSessionCreateResult(send, message.id, { owner: "shell", error: "missing source" });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
sendSessionCreateResult(send, message.id, { owner: "shell", error: "unsupported owner mode" });
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
state.sendHandles.set(windowId, send);
|
|
1309
|
+
const sessionId = canonicalizeSessionId(state, windowId, message.sessionId);
|
|
1310
|
+
const entry = {
|
|
1311
|
+
sessionId,
|
|
1312
|
+
windowId,
|
|
1313
|
+
owner: message.owner,
|
|
1314
|
+
source: message.source,
|
|
1315
|
+
metadata: message.metadata,
|
|
1316
|
+
state: void 0,
|
|
1317
|
+
actions: message.capabilities ?? DEFAULT_MEDIA_ACTIONS,
|
|
1318
|
+
lastTouched: ++state.touchCounter
|
|
1319
|
+
};
|
|
1320
|
+
state.sessionRegistry.set(sessionId, entry);
|
|
1321
|
+
registerWindowSession(state, windowId, sessionId);
|
|
1322
|
+
setActive(state, sessionId, entry.actions);
|
|
1323
|
+
state.options.onSessionCreate?.(windowId, sessionId, message.metadata);
|
|
1324
|
+
sendSessionCreateResult(send, message.id, { sessionId, owner: message.owner });
|
|
1325
|
+
}
|
|
1326
|
+
function handleSessionUpdate(state, windowId, message) {
|
|
1327
|
+
const entry = state.sessionRegistry.get(message.sessionId);
|
|
1328
|
+
if (entry) {
|
|
1329
|
+
entry.metadata = { ...entry.metadata, ...message.metadata };
|
|
1330
|
+
entry.lastTouched = ++state.touchCounter;
|
|
1331
|
+
if (entry.owner === "napplet" && message.sessionId === state.activeSessionId && entry.metadata) {
|
|
1332
|
+
state.bridge.setMetadata(message.sessionId, entry.metadata);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
state.options.onSessionUpdate?.(windowId, message.sessionId, message.metadata);
|
|
1336
|
+
}
|
|
1337
|
+
function handleSessionDestroy(state, windowId, message) {
|
|
1338
|
+
const entry = state.sessionRegistry.get(message.sessionId);
|
|
1339
|
+
if (entry) {
|
|
1340
|
+
state.sessionRegistry.delete(message.sessionId);
|
|
1341
|
+
const set = state.windowSessions.get(entry.windowId);
|
|
1342
|
+
if (set) {
|
|
1343
|
+
set.delete(message.sessionId);
|
|
1344
|
+
if (set.size === 0) state.windowSessions.delete(entry.windowId);
|
|
1345
|
+
}
|
|
1346
|
+
state.bridge.destroySession?.(message.sessionId);
|
|
1347
|
+
if (message.sessionId === state.activeSessionId) promoteNextActiveOrClear(state);
|
|
1348
|
+
}
|
|
1349
|
+
state.options.onSessionDestroy?.(windowId, message.sessionId);
|
|
1350
|
+
}
|
|
1351
|
+
function handleMediaState(state, windowId, message) {
|
|
1352
|
+
const entry = state.sessionRegistry.get(message.sessionId);
|
|
1353
|
+
if (entry?.owner === "napplet") {
|
|
1354
|
+
entry.state = {
|
|
1355
|
+
status: message.status,
|
|
1356
|
+
position: message.position,
|
|
1357
|
+
duration: message.duration,
|
|
1358
|
+
volume: message.volume
|
|
1359
|
+
};
|
|
1360
|
+
entry.lastTouched = ++state.touchCounter;
|
|
1361
|
+
if (state.activeSessionId !== message.sessionId) setActive(state, message.sessionId, entry.actions);
|
|
1362
|
+
else state.bridge.setPlaybackState(message.sessionId, message.status);
|
|
1363
|
+
}
|
|
1364
|
+
state.options.onState?.(windowId, message.sessionId, message);
|
|
1365
|
+
}
|
|
1366
|
+
function handleMediaCapabilities(state, windowId, message) {
|
|
1367
|
+
const entry = state.sessionRegistry.get(message.sessionId);
|
|
1368
|
+
if (entry?.owner === "napplet") {
|
|
1369
|
+
entry.actions = message.actions;
|
|
1370
|
+
entry.lastTouched = ++state.touchCounter;
|
|
1371
|
+
if (message.sessionId === state.activeSessionId) {
|
|
1372
|
+
state.bridge.setActiveSession?.(message.sessionId, entry.actions);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
state.options.onCapabilities?.(windowId, message.sessionId, message.actions);
|
|
1376
|
+
}
|
|
1377
|
+
function handleMediaMessage(state, windowId, message, send) {
|
|
1378
|
+
switch (message.type) {
|
|
1379
|
+
case "media.session.create":
|
|
1380
|
+
handleSessionCreate(state, windowId, message, send);
|
|
1381
|
+
return;
|
|
1382
|
+
case "media.session.update":
|
|
1383
|
+
handleSessionUpdate(state, windowId, message);
|
|
1384
|
+
return;
|
|
1385
|
+
case "media.session.destroy":
|
|
1386
|
+
handleSessionDestroy(state, windowId, message);
|
|
1387
|
+
return;
|
|
1388
|
+
case "media.state":
|
|
1389
|
+
handleMediaState(state, windowId, message);
|
|
1390
|
+
return;
|
|
1391
|
+
case "media.capabilities":
|
|
1392
|
+
handleMediaCapabilities(state, windowId, message);
|
|
1393
|
+
return;
|
|
1394
|
+
default: {
|
|
1395
|
+
const id = message.id ?? "";
|
|
1396
|
+
send({
|
|
1397
|
+
type: `${message.type}.error`,
|
|
1398
|
+
id,
|
|
1399
|
+
error: `Unknown media method: ${message.type}`
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function destroyWindowSessions(state, windowId) {
|
|
1405
|
+
const sessions = state.windowSessions.get(windowId);
|
|
1406
|
+
if (sessions) {
|
|
1407
|
+
const ownedActive = state.activeSessionId !== null && sessions.has(state.activeSessionId);
|
|
1408
|
+
for (const sessionId of sessions) {
|
|
1409
|
+
state.sessionRegistry.delete(sessionId);
|
|
1410
|
+
state.bridge.destroySession?.(sessionId);
|
|
1411
|
+
}
|
|
1412
|
+
state.windowSessions.delete(windowId);
|
|
1413
|
+
if (ownedActive) promoteNextActiveOrClear(state);
|
|
1414
|
+
}
|
|
1415
|
+
state.sendHandles.delete(windowId);
|
|
1416
|
+
}
|
|
1417
|
+
function destroyMediaState(state, unsubscribeAction) {
|
|
1418
|
+
unsubscribeAction();
|
|
1419
|
+
for (const sessionId of state.sessionRegistry.keys()) state.bridge.destroySession?.(sessionId);
|
|
1420
|
+
state.bridge.setActiveSession?.(null);
|
|
1421
|
+
state.sessionRegistry.clear();
|
|
1422
|
+
state.windowSessions.clear();
|
|
1423
|
+
state.sendHandles.clear();
|
|
1424
|
+
state.activeSessionId = null;
|
|
1425
|
+
state.touchCounter = 0;
|
|
1426
|
+
state.sessionCounter = 0;
|
|
1427
|
+
}
|
|
706
1428
|
function createMediaService(options = {}) {
|
|
707
1429
|
const descriptor = {
|
|
708
1430
|
name: "media",
|
|
709
1431
|
version: MEDIA_SERVICE_VERSION,
|
|
710
|
-
description: "NIP-5D media NUB reference handler (
|
|
1432
|
+
description: options.hostBridge ? "NIP-5D media NUB reference handler (host-bridge delegated)" : "NIP-5D media NUB reference handler (navigator.mediaSession mirror)"
|
|
711
1433
|
};
|
|
1434
|
+
const bridge = options.hostBridge ?? createBrowserMediaBridge({
|
|
1435
|
+
mediaSessionTarget: options.mediaSessionTarget,
|
|
1436
|
+
documentTarget: options.documentTarget
|
|
1437
|
+
});
|
|
1438
|
+
const state = createMediaServiceState(options, bridge);
|
|
1439
|
+
const unsubscribeAction = bridge.onAction((sessionId, action, value) => {
|
|
1440
|
+
sendMediaCommand(state, sessionId, action, value);
|
|
1441
|
+
});
|
|
712
1442
|
return {
|
|
713
1443
|
descriptor,
|
|
714
1444
|
handleMessage(windowId, message, send) {
|
|
715
|
-
|
|
716
|
-
case "media.session.create": {
|
|
717
|
-
const m = message;
|
|
718
|
-
options.onSessionCreate?.(windowId, m.sessionId, m.metadata);
|
|
719
|
-
const result = {
|
|
720
|
-
type: "media.session.create.result",
|
|
721
|
-
id: m.id,
|
|
722
|
-
sessionId: m.sessionId
|
|
723
|
-
};
|
|
724
|
-
send(result);
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
case "media.session.update": {
|
|
728
|
-
const m = message;
|
|
729
|
-
options.onSessionUpdate?.(windowId, m.sessionId ?? "", m.metadata);
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
case "media.session.destroy": {
|
|
733
|
-
const m = message;
|
|
734
|
-
options.onSessionDestroy?.(windowId, m.sessionId ?? "");
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
case "media.state": {
|
|
738
|
-
const m = message;
|
|
739
|
-
options.onState?.(windowId, m.sessionId ?? "", m);
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
case "media.capabilities": {
|
|
743
|
-
const m = message;
|
|
744
|
-
options.onCapabilities?.(windowId, m.sessionId ?? "", m.actions);
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
default: {
|
|
748
|
-
const id = message.id ?? "";
|
|
749
|
-
send({
|
|
750
|
-
type: `${message.type}.error`,
|
|
751
|
-
id,
|
|
752
|
-
error: `Unknown media method: ${message.type}`
|
|
753
|
-
});
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
1445
|
+
handleMediaMessage(state, windowId, message, send);
|
|
757
1446
|
},
|
|
758
|
-
onWindowDestroyed(
|
|
1447
|
+
onWindowDestroyed(windowId) {
|
|
1448
|
+
destroyWindowSessions(state, windowId);
|
|
1449
|
+
},
|
|
1450
|
+
destroy() {
|
|
1451
|
+
destroyMediaState(state, unsubscribeAction);
|
|
759
1452
|
}
|
|
760
1453
|
};
|
|
761
1454
|
}
|
|
@@ -827,7 +1520,7 @@ var DEFAULT_THEME = {
|
|
|
827
1520
|
primary: "#7aa2f7"
|
|
828
1521
|
}
|
|
829
1522
|
// fonts, background, title intentionally undefined — all optional per
|
|
830
|
-
// @napplet/nub
|
|
1523
|
+
// @napplet/nub/theme Theme interface.
|
|
831
1524
|
};
|
|
832
1525
|
function createThemeService(options = {}) {
|
|
833
1526
|
let currentTheme = options.initialTheme ?? DEFAULT_THEME;
|
|
@@ -870,16 +1563,842 @@ function createThemeService(options = {}) {
|
|
|
870
1563
|
}
|
|
871
1564
|
return { handler, publishTheme, getCurrentTheme };
|
|
872
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
// src/config-service.ts
|
|
1568
|
+
var CONFIG_SERVICE_VERSION = "1.0.0";
|
|
1569
|
+
function validateCoreSubset(schema) {
|
|
1570
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
1571
|
+
return { ok: false, code: "invalid-schema", error: "schema root must be an object" };
|
|
1572
|
+
}
|
|
1573
|
+
const s = schema;
|
|
1574
|
+
if ("$ref" in s) {
|
|
1575
|
+
return { ok: false, code: "ref-not-allowed", error: "$ref is not permitted in the Core Subset" };
|
|
1576
|
+
}
|
|
1577
|
+
if ("pattern" in s) {
|
|
1578
|
+
return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
code: "pattern-not-allowed",
|
|
1581
|
+
error: "pattern is not permitted in the Core Subset"
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if ("oneOf" in s || "anyOf" in s || "allOf" in s || "not" in s) {
|
|
1585
|
+
return {
|
|
1586
|
+
ok: false,
|
|
1587
|
+
code: "invalid-schema",
|
|
1588
|
+
error: "oneOf/anyOf/allOf/not are not permitted in the Core Subset"
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
if ("if" in s || "then" in s || "else" in s) {
|
|
1592
|
+
return {
|
|
1593
|
+
ok: false,
|
|
1594
|
+
code: "invalid-schema",
|
|
1595
|
+
error: "if/then/else are not permitted in the Core Subset"
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
if (s.type !== "object") {
|
|
1599
|
+
return { ok: false, code: "invalid-schema", error: 'schema root must have type: "object"' };
|
|
1600
|
+
}
|
|
1601
|
+
const props = s.properties;
|
|
1602
|
+
if (props !== void 0 && (typeof props !== "object" || props === null)) {
|
|
1603
|
+
return { ok: false, code: "invalid-schema", error: "properties must be an object" };
|
|
1604
|
+
}
|
|
1605
|
+
if (props) {
|
|
1606
|
+
for (const [key, val] of Object.entries(props)) {
|
|
1607
|
+
if (typeof val !== "object" || val === null) {
|
|
1608
|
+
return {
|
|
1609
|
+
ok: false,
|
|
1610
|
+
code: "invalid-schema",
|
|
1611
|
+
error: `property "${key}" must be an object schema`
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
const pv = val;
|
|
1615
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "array", "object"]);
|
|
1616
|
+
if (pv.type !== void 0 && !ALLOWED_TYPES.has(pv.type)) {
|
|
1617
|
+
return {
|
|
1618
|
+
ok: false,
|
|
1619
|
+
code: "invalid-schema",
|
|
1620
|
+
error: `property "${key}" must have type: string|number|boolean|array|object`
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return { ok: true };
|
|
1626
|
+
}
|
|
1627
|
+
function createConfigService(options) {
|
|
1628
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
1629
|
+
const descriptor = {
|
|
1630
|
+
name: "config",
|
|
1631
|
+
version: CONFIG_SERVICE_VERSION,
|
|
1632
|
+
description: "NUB-CONFIG reference service \u2014 shell-writes, napplet-reads configuration"
|
|
1633
|
+
};
|
|
1634
|
+
const handler = {
|
|
1635
|
+
descriptor,
|
|
1636
|
+
handleMessage(windowId, message, send) {
|
|
1637
|
+
switch (message.type) {
|
|
1638
|
+
case "config.get": {
|
|
1639
|
+
const m = message;
|
|
1640
|
+
const reply = {
|
|
1641
|
+
type: "config.values",
|
|
1642
|
+
id: m.id,
|
|
1643
|
+
values: options.getValues()
|
|
1644
|
+
};
|
|
1645
|
+
send(reply);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
case "config.subscribe": {
|
|
1649
|
+
subscribers.set(windowId, send);
|
|
1650
|
+
const push = {
|
|
1651
|
+
type: "config.values",
|
|
1652
|
+
values: options.getValues()
|
|
1653
|
+
};
|
|
1654
|
+
send(push);
|
|
1655
|
+
options.onSubscribe?.(windowId);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
case "config.unsubscribe": {
|
|
1659
|
+
subscribers.delete(windowId);
|
|
1660
|
+
options.onUnsubscribe?.(windowId);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
case "config.registerSchema": {
|
|
1664
|
+
const m = message;
|
|
1665
|
+
const validation = options.registerSchema ? options.registerSchema(windowId, m.schema, m.version) : validateCoreSubset(m.schema);
|
|
1666
|
+
const result = validation.ok ? { type: "config.registerSchema.result", id: m.id, ok: true } : {
|
|
1667
|
+
type: "config.registerSchema.result",
|
|
1668
|
+
id: m.id,
|
|
1669
|
+
ok: false,
|
|
1670
|
+
code: validation.code,
|
|
1671
|
+
error: validation.error
|
|
1672
|
+
};
|
|
1673
|
+
send(result);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
case "config.openSettings": {
|
|
1677
|
+
const m = message;
|
|
1678
|
+
options.openSettings?.(windowId, m.section);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
default:
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
},
|
|
1685
|
+
onWindowDestroyed(windowId) {
|
|
1686
|
+
subscribers.delete(windowId);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
function publishValues(values) {
|
|
1690
|
+
const envelope = {
|
|
1691
|
+
type: "config.values",
|
|
1692
|
+
values
|
|
1693
|
+
};
|
|
1694
|
+
for (const send of subscribers.values()) {
|
|
1695
|
+
try {
|
|
1696
|
+
send(envelope);
|
|
1697
|
+
} catch {
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return { handler, publishValues };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/resource-service.ts
|
|
1705
|
+
var RESOURCE_SERVICE_VERSION = "1.0.0";
|
|
1706
|
+
function arrayBufferToBase64(buf) {
|
|
1707
|
+
const bytes = new Uint8Array(buf);
|
|
1708
|
+
const CHUNK = 32768;
|
|
1709
|
+
let binary = "";
|
|
1710
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
1711
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
1712
|
+
}
|
|
1713
|
+
return btoa(binary);
|
|
1714
|
+
}
|
|
1715
|
+
function assertResourceOptions(options) {
|
|
1716
|
+
if (typeof options?.fetch !== "function" || typeof options?.isOriginGranted !== "function" || typeof options?.getConnectGrants !== "function" || typeof options?.resolveIdentity !== "function") {
|
|
1717
|
+
throw new Error(
|
|
1718
|
+
"[RESOURCE-01 / H-03] createResourceService requires {fetch, isOriginGranted, getConnectGrants, resolveIdentity} \u2014 all four options are required from day one. The grants source (getConnectGrants) MUST be wired at construction time to prevent unguarded fetch proxying."
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function trackRequest(state, requestId, windowId, controller) {
|
|
1723
|
+
state.inFlight.set(requestId, { controller, windowId });
|
|
1724
|
+
if (!state.perWindow.has(windowId)) {
|
|
1725
|
+
state.perWindow.set(windowId, /* @__PURE__ */ new Set());
|
|
1726
|
+
}
|
|
1727
|
+
state.perWindow.get(windowId).add(requestId);
|
|
1728
|
+
}
|
|
1729
|
+
function untrackRequest(state, requestId) {
|
|
1730
|
+
const entry = state.inFlight.get(requestId);
|
|
1731
|
+
if (entry) {
|
|
1732
|
+
state.inFlight.delete(requestId);
|
|
1733
|
+
state.perWindow.get(entry.windowId)?.delete(requestId);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
function sendResourceError(send, requestId, code, message) {
|
|
1737
|
+
send({
|
|
1738
|
+
type: "resource.bytes.error",
|
|
1739
|
+
requestId,
|
|
1740
|
+
code,
|
|
1741
|
+
message
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
function parseResourceUrl(send, requestId, url) {
|
|
1745
|
+
try {
|
|
1746
|
+
return new URL(url);
|
|
1747
|
+
} catch {
|
|
1748
|
+
sendResourceError(send, requestId, "invalid-url", `invalid URL: ${url}`);
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function collectResponseHeaders(response) {
|
|
1753
|
+
const headers = {};
|
|
1754
|
+
response.headers.forEach((value, key) => {
|
|
1755
|
+
headers[key] = value;
|
|
1756
|
+
});
|
|
1757
|
+
return headers;
|
|
1758
|
+
}
|
|
1759
|
+
async function handleBytes(options, state, windowId, msg, send) {
|
|
1760
|
+
const { requestId, url, init } = msg;
|
|
1761
|
+
const identity = options.resolveIdentity(windowId);
|
|
1762
|
+
if (!identity) {
|
|
1763
|
+
sendResourceError(send, requestId, "denied", "napplet identity not resolvable");
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const parsedUrl = parseResourceUrl(send, requestId, url);
|
|
1767
|
+
if (!parsedUrl) return;
|
|
1768
|
+
const origin = parsedUrl.origin;
|
|
1769
|
+
const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
|
|
1770
|
+
if (!options.isOriginGranted(origin, grants)) {
|
|
1771
|
+
sendResourceError(send, requestId, "denied", `origin ${origin} not granted`);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const controller = new AbortController();
|
|
1775
|
+
trackRequest(state, requestId, windowId, controller);
|
|
1776
|
+
try {
|
|
1777
|
+
const response = await options.fetch(url, {
|
|
1778
|
+
method: init?.method,
|
|
1779
|
+
headers: init?.headers ? { ...init.headers } : void 0,
|
|
1780
|
+
signal: controller.signal
|
|
1781
|
+
});
|
|
1782
|
+
const buffer = await response.arrayBuffer();
|
|
1783
|
+
send({
|
|
1784
|
+
type: "resource.bytes.result",
|
|
1785
|
+
requestId,
|
|
1786
|
+
status: response.status,
|
|
1787
|
+
headers: collectResponseHeaders(response),
|
|
1788
|
+
bodyBase64: arrayBufferToBase64(buffer)
|
|
1789
|
+
});
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
const isAbort = controller.signal.aborted || err instanceof Error && (err.name === "AbortError" || err.name === "DOMException");
|
|
1792
|
+
sendResourceError(
|
|
1793
|
+
send,
|
|
1794
|
+
requestId,
|
|
1795
|
+
isAbort ? "canceled" : "network-error",
|
|
1796
|
+
err instanceof Error ? err.message : String(err)
|
|
1797
|
+
);
|
|
1798
|
+
} finally {
|
|
1799
|
+
untrackRequest(state, requestId);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function handleCancel(state, requestId) {
|
|
1803
|
+
const entry = state.inFlight.get(requestId);
|
|
1804
|
+
if (entry) {
|
|
1805
|
+
entry.controller.abort();
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function destroyWindowRequests(state, windowId) {
|
|
1809
|
+
const requestIds = state.perWindow.get(windowId);
|
|
1810
|
+
if (!requestIds) return;
|
|
1811
|
+
for (const requestId of requestIds) {
|
|
1812
|
+
const entry = state.inFlight.get(requestId);
|
|
1813
|
+
if (entry) {
|
|
1814
|
+
entry.controller.abort();
|
|
1815
|
+
state.inFlight.delete(requestId);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
state.perWindow.delete(windowId);
|
|
1819
|
+
}
|
|
1820
|
+
function createResourceService(options) {
|
|
1821
|
+
assertResourceOptions(options);
|
|
1822
|
+
const state = {
|
|
1823
|
+
inFlight: /* @__PURE__ */ new Map(),
|
|
1824
|
+
perWindow: /* @__PURE__ */ new Map()
|
|
1825
|
+
};
|
|
1826
|
+
const descriptor = {
|
|
1827
|
+
name: "resource",
|
|
1828
|
+
version: RESOURCE_SERVICE_VERSION,
|
|
1829
|
+
description: "NUB-RESOURCE reference service \u2014 shell-proxied authenticated fetch (RESOURCE-01..06)"
|
|
1830
|
+
};
|
|
1831
|
+
const handler = {
|
|
1832
|
+
descriptor,
|
|
1833
|
+
handleMessage(windowId, message, send) {
|
|
1834
|
+
switch (message.type) {
|
|
1835
|
+
case "resource.bytes": {
|
|
1836
|
+
const m = message;
|
|
1837
|
+
handleBytes(options, state, windowId, m, send).catch(() => {
|
|
1838
|
+
});
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
case "resource.cancel": {
|
|
1842
|
+
const m = message;
|
|
1843
|
+
handleCancel(state, m.requestId);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
default:
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
onWindowDestroyed(windowId) {
|
|
1851
|
+
destroyWindowRequests(state, windowId);
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
return handler;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/outbox-service.ts
|
|
1858
|
+
var OUTBOX_SERVICE_VERSION = "1.0.0";
|
|
1859
|
+
var OUTBOX_DESCRIPTOR = {
|
|
1860
|
+
name: "outbox",
|
|
1861
|
+
version: OUTBOX_SERVICE_VERSION,
|
|
1862
|
+
description: "NAP-OUTBOX outbox-aware relay routing \u2014 query/subscribe/publish/resolveRelays"
|
|
1863
|
+
};
|
|
1864
|
+
function normalizeFilters(raw) {
|
|
1865
|
+
if (Array.isArray(raw)) {
|
|
1866
|
+
const filters = raw.filter((f) => typeof f === "object" && f !== null);
|
|
1867
|
+
return filters.length > 0 ? filters : null;
|
|
1868
|
+
}
|
|
1869
|
+
if (typeof raw === "object" && raw !== null) return [raw];
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
function createOutboxService(options) {
|
|
1873
|
+
if (!options || typeof options.router !== "object" || options.router === null) {
|
|
1874
|
+
throw new Error("createOutboxService: options.router is required");
|
|
1875
|
+
}
|
|
1876
|
+
const { router } = options;
|
|
1877
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
1878
|
+
function handleQuery(msg, send) {
|
|
1879
|
+
const m = msg;
|
|
1880
|
+
const id = m.id ?? "";
|
|
1881
|
+
const filters = normalizeFilters(m.filters);
|
|
1882
|
+
if (!filters) {
|
|
1883
|
+
send({ type: "outbox.query.result", id, events: [], relays: {}, error: "invalid filter" });
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
void router.query(filters, m.options).then(
|
|
1887
|
+
(result) => send({
|
|
1888
|
+
type: "outbox.query.result",
|
|
1889
|
+
id,
|
|
1890
|
+
events: result.events,
|
|
1891
|
+
relays: result.relays,
|
|
1892
|
+
...result.incomplete === void 0 ? {} : { incomplete: result.incomplete },
|
|
1893
|
+
...result.error === void 0 ? {} : { error: result.error }
|
|
1894
|
+
})
|
|
1895
|
+
).catch(
|
|
1896
|
+
(err) => send({ type: "outbox.query.result", id, events: [], relays: {}, error: toErrorMessage(err) })
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
function handleSubscribe(windowId, msg, send) {
|
|
1900
|
+
const m = msg;
|
|
1901
|
+
const subId = m.subId;
|
|
1902
|
+
if (typeof subId !== "string" || subId.length === 0) return;
|
|
1903
|
+
const subKey = `${windowId}:${subId}`;
|
|
1904
|
+
subscriptions.get(subKey)?.close();
|
|
1905
|
+
subscriptions.delete(subKey);
|
|
1906
|
+
const filters = normalizeFilters(m.filters);
|
|
1907
|
+
if (!filters) {
|
|
1908
|
+
send({ type: "outbox.closed", subId, reason: "invalid filter" });
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const sink = {
|
|
1912
|
+
event: (event, relay) => send({ type: "outbox.event", subId, event, ...relay === void 0 ? {} : { relay } }),
|
|
1913
|
+
eose: () => send({ type: "outbox.eose", subId }),
|
|
1914
|
+
closed: (reason) => {
|
|
1915
|
+
subscriptions.delete(subKey);
|
|
1916
|
+
send({ type: "outbox.closed", subId, ...reason === void 0 ? {} : { reason } });
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
subscriptions.set(subKey, router.subscribe(filters, m.options, sink));
|
|
1920
|
+
}
|
|
1921
|
+
function handleClose(windowId, msg, send) {
|
|
1922
|
+
const m = msg;
|
|
1923
|
+
const subId = m.subId;
|
|
1924
|
+
if (typeof subId !== "string") return;
|
|
1925
|
+
const subKey = `${windowId}:${subId}`;
|
|
1926
|
+
subscriptions.get(subKey)?.close();
|
|
1927
|
+
subscriptions.delete(subKey);
|
|
1928
|
+
send({ type: "outbox.closed", subId });
|
|
1929
|
+
}
|
|
1930
|
+
function handlePublish(msg, send) {
|
|
1931
|
+
const m = msg;
|
|
1932
|
+
const id = m.id ?? "";
|
|
1933
|
+
if (!m.event || typeof m.event !== "object") {
|
|
1934
|
+
send({ type: "outbox.publish.result", id, ok: false, error: "invalid filter" });
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
void router.publish(m.event, m.options).then(
|
|
1938
|
+
(result) => send({
|
|
1939
|
+
type: "outbox.publish.result",
|
|
1940
|
+
id,
|
|
1941
|
+
ok: result.ok,
|
|
1942
|
+
...result.event === void 0 ? {} : { event: result.event },
|
|
1943
|
+
...result.eventId === void 0 ? {} : { eventId: result.eventId },
|
|
1944
|
+
...result.relays === void 0 ? {} : { relays: result.relays },
|
|
1945
|
+
...result.error === void 0 ? {} : { error: result.error }
|
|
1946
|
+
})
|
|
1947
|
+
).catch(
|
|
1948
|
+
(err) => send({ type: "outbox.publish.result", id, ok: false, error: toErrorMessage(err) })
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
function handleResolveRelays(msg, send) {
|
|
1952
|
+
const m = msg;
|
|
1953
|
+
const id = m.id ?? "";
|
|
1954
|
+
if (!m.target || typeof m.target !== "object") {
|
|
1955
|
+
send({ type: "outbox.resolveRelays.result", id, error: "invalid filter" });
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
void router.resolveRelays(m.target).then((plan) => send({ type: "outbox.resolveRelays.result", id, plan })).catch(
|
|
1959
|
+
(err) => send({ type: "outbox.resolveRelays.result", id, error: toErrorMessage(err) })
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
return {
|
|
1963
|
+
descriptor: OUTBOX_DESCRIPTOR,
|
|
1964
|
+
handleMessage(windowId, message, send) {
|
|
1965
|
+
switch (message.type) {
|
|
1966
|
+
case "outbox.query":
|
|
1967
|
+
handleQuery(message, send);
|
|
1968
|
+
return;
|
|
1969
|
+
case "outbox.subscribe":
|
|
1970
|
+
handleSubscribe(windowId, message, send);
|
|
1971
|
+
return;
|
|
1972
|
+
case "outbox.close":
|
|
1973
|
+
handleClose(windowId, message, send);
|
|
1974
|
+
return;
|
|
1975
|
+
case "outbox.publish":
|
|
1976
|
+
handlePublish(message, send);
|
|
1977
|
+
return;
|
|
1978
|
+
case "outbox.resolveRelays":
|
|
1979
|
+
handleResolveRelays(message, send);
|
|
1980
|
+
return;
|
|
1981
|
+
default:
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
},
|
|
1985
|
+
onWindowDestroyed(windowId) {
|
|
1986
|
+
const prefix = `${windowId}:`;
|
|
1987
|
+
for (const [key, sub] of subscriptions) {
|
|
1988
|
+
if (key.startsWith(prefix)) {
|
|
1989
|
+
sub.close();
|
|
1990
|
+
subscriptions.delete(key);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
function toErrorMessage(err) {
|
|
1997
|
+
if (err instanceof Error) return err.message;
|
|
1998
|
+
if (typeof err === "string") return err;
|
|
1999
|
+
return "outbox request failed";
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// src/relay-pool-outbox-router.ts
|
|
2003
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 4e3;
|
|
2004
|
+
function defaultRelayAllowed(url) {
|
|
2005
|
+
return typeof url === "string" && (url.startsWith("wss://") || url.startsWith("ws://"));
|
|
2006
|
+
}
|
|
2007
|
+
function deriveAuthors(filters, optionAuthors) {
|
|
2008
|
+
const authors = /* @__PURE__ */ new Set();
|
|
2009
|
+
for (const filter of filters) {
|
|
2010
|
+
for (const author of filter.authors ?? []) authors.add(author);
|
|
2011
|
+
}
|
|
2012
|
+
for (const author of optionAuthors ?? []) authors.add(author);
|
|
2013
|
+
return [...authors];
|
|
2014
|
+
}
|
|
2015
|
+
function wantsWriteRelays(direction, strategy) {
|
|
2016
|
+
if (strategy === "outbox") return true;
|
|
2017
|
+
if (strategy === "inbox") return false;
|
|
2018
|
+
return direction === "read";
|
|
2019
|
+
}
|
|
2020
|
+
function allowed(ctx, urls) {
|
|
2021
|
+
const out = /* @__PURE__ */ new Set();
|
|
2022
|
+
for (const url of urls) {
|
|
2023
|
+
if (ctx.isRelayAllowed(url)) out.add(url);
|
|
2024
|
+
}
|
|
2025
|
+
return [...out];
|
|
2026
|
+
}
|
|
2027
|
+
async function resolvePlan(ctx, pubkeys, direction, strategy, relayHints) {
|
|
2028
|
+
const useWrite = wantsWriteRelays(direction, strategy);
|
|
2029
|
+
const collected = /* @__PURE__ */ new Set();
|
|
2030
|
+
const missingAuthors = [];
|
|
2031
|
+
let sawNip65 = false;
|
|
2032
|
+
if (pubkeys.length > 0) {
|
|
2033
|
+
const lists = await ctx.loadRelayLists(pubkeys);
|
|
2034
|
+
for (const pubkey of pubkeys) {
|
|
2035
|
+
const entry = lists.get(pubkey);
|
|
2036
|
+
const relays2 = entry ? useWrite ? entry.write : entry.read : void 0;
|
|
2037
|
+
if (relays2 && relays2.length > 0) {
|
|
2038
|
+
sawNip65 = true;
|
|
2039
|
+
for (const url of relays2) collected.add(url);
|
|
2040
|
+
} else {
|
|
2041
|
+
missingAuthors.push(pubkey);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
for (const url of relayHints ?? []) collected.add(url);
|
|
2046
|
+
let relays = allowed(ctx, collected);
|
|
2047
|
+
let source;
|
|
2048
|
+
if (relays.length === 0) {
|
|
2049
|
+
relays = allowed(ctx, ctx.fallbackRelays);
|
|
2050
|
+
source = "fallback";
|
|
2051
|
+
} else {
|
|
2052
|
+
source = sawNip65 ? "nip65" : "policy";
|
|
2053
|
+
}
|
|
2054
|
+
const plan = { relays, source };
|
|
2055
|
+
if (missingAuthors.length > 0) plan.missingAuthors = missingAuthors;
|
|
2056
|
+
return plan;
|
|
2057
|
+
}
|
|
2058
|
+
function recordRelay(collector, id, relayUrl) {
|
|
2059
|
+
let set = collector.relayMap.get(id);
|
|
2060
|
+
if (!set) {
|
|
2061
|
+
set = /* @__PURE__ */ new Set();
|
|
2062
|
+
collector.relayMap.set(id, set);
|
|
2063
|
+
}
|
|
2064
|
+
set.add(relayUrl);
|
|
2065
|
+
}
|
|
2066
|
+
function admitEvent(ctx, collector, event) {
|
|
2067
|
+
if (collector.seen.has(event.id)) return;
|
|
2068
|
+
collector.verifications.push(
|
|
2069
|
+
ctx.verify(event).then((ok) => {
|
|
2070
|
+
if (ok && !collector.seen.has(event.id)) collector.seen.set(event.id, event);
|
|
2071
|
+
else if (!ok) collector.relayMap.delete(event.id);
|
|
2072
|
+
})
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
function buildCollectResult(collector, timedOut) {
|
|
2076
|
+
const events = [...collector.seen.values()];
|
|
2077
|
+
const relayObj = {};
|
|
2078
|
+
for (const event of events) relayObj[event.id] = [...collector.relayMap.get(event.id) ?? []];
|
|
2079
|
+
return { events, relayMap: relayObj, incomplete: timedOut };
|
|
2080
|
+
}
|
|
2081
|
+
function collectFromRelays(ctx, filters, relayUrls, timeoutMs) {
|
|
2082
|
+
return new Promise((resolve) => {
|
|
2083
|
+
const collector = { seen: /* @__PURE__ */ new Map(), relayMap: /* @__PURE__ */ new Map(), verifications: [] };
|
|
2084
|
+
const handles = [];
|
|
2085
|
+
let eoseCount = 0;
|
|
2086
|
+
let finished = false;
|
|
2087
|
+
let timedOut = false;
|
|
2088
|
+
function finalize() {
|
|
2089
|
+
if (finished) return;
|
|
2090
|
+
finished = true;
|
|
2091
|
+
clearTimeout(timer);
|
|
2092
|
+
for (const handle of handles) {
|
|
2093
|
+
try {
|
|
2094
|
+
handle.unsubscribe();
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
void Promise.all(collector.verifications).then(() => resolve(buildCollectResult(collector, timedOut)));
|
|
2099
|
+
}
|
|
2100
|
+
const timer = setTimeout(() => {
|
|
2101
|
+
timedOut = true;
|
|
2102
|
+
finalize();
|
|
2103
|
+
}, timeoutMs);
|
|
2104
|
+
for (const relayUrl of relayUrls) {
|
|
2105
|
+
handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2106
|
+
if (finished) return;
|
|
2107
|
+
if (item === "EOSE") {
|
|
2108
|
+
eoseCount += 1;
|
|
2109
|
+
if (eoseCount >= relayUrls.length) finalize();
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
recordRelay(collector, item.id, relayUrl);
|
|
2113
|
+
admitEvent(ctx, collector, item);
|
|
2114
|
+
}));
|
|
2115
|
+
}
|
|
2116
|
+
if (relayUrls.length === 0) finalize();
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
function relayPoolSubscribe(ctx, filters, relayUrl, cb) {
|
|
2120
|
+
return ctx.relayPool.subscribe(filters, [relayUrl], cb);
|
|
2121
|
+
}
|
|
2122
|
+
async function queryImpl(ctx, filters, options) {
|
|
2123
|
+
if (!ctx.relayPool.isAvailable()) {
|
|
2124
|
+
return { events: [], relays: {}, incomplete: true, error: "relay list unavailable" };
|
|
2125
|
+
}
|
|
2126
|
+
const strategy = options?.strategy ?? "auto";
|
|
2127
|
+
const authors = deriveAuthors(filters, options?.authors);
|
|
2128
|
+
const plan = await resolvePlan(ctx, authors, "read", strategy, options?.relays);
|
|
2129
|
+
if (plan.relays.length === 0) {
|
|
2130
|
+
return { events: [], relays: {}, incomplete: true, error: "relay list unavailable" };
|
|
2131
|
+
}
|
|
2132
|
+
const timeoutMs = options?.timeoutMs ?? ctx.defaultTimeoutMs;
|
|
2133
|
+
const collected = await collectFromRelays(ctx, filters, plan.relays, timeoutMs);
|
|
2134
|
+
const incomplete = collected.incomplete || (plan.missingAuthors?.length ?? 0) > 0;
|
|
2135
|
+
let events = collected.events;
|
|
2136
|
+
if (options?.limit !== void 0 && events.length > options.limit) {
|
|
2137
|
+
events = [...events].sort((a, b) => b.created_at - a.created_at).slice(0, options.limit);
|
|
2138
|
+
}
|
|
2139
|
+
const result = { events, relays: collected.relayMap };
|
|
2140
|
+
if (incomplete) result.incomplete = true;
|
|
2141
|
+
return result;
|
|
2142
|
+
}
|
|
2143
|
+
function closeLiveSub(sub) {
|
|
2144
|
+
for (const handle of sub.handles) {
|
|
2145
|
+
try {
|
|
2146
|
+
handle.unsubscribe();
|
|
2147
|
+
} catch {
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
sub.handles.length = 0;
|
|
2151
|
+
}
|
|
2152
|
+
function attachLiveRelay(ctx, sub, filters, relayUrl, live, sink) {
|
|
2153
|
+
sub.handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2154
|
+
if (sub.closed) return;
|
|
2155
|
+
if (item === "EOSE") {
|
|
2156
|
+
sub.eoseCount += 1;
|
|
2157
|
+
if (!sub.eoseSent && sub.eoseCount >= sub.relayCount) {
|
|
2158
|
+
sub.eoseSent = true;
|
|
2159
|
+
sink.eose();
|
|
2160
|
+
if (!live) {
|
|
2161
|
+
sub.closed = true;
|
|
2162
|
+
closeLiveSub(sub);
|
|
2163
|
+
sink.closed();
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
if (sub.seen.has(item.id)) return;
|
|
2169
|
+
void ctx.verify(item).then((ok) => {
|
|
2170
|
+
if (!ok || sub.closed || sub.seen.has(item.id)) return;
|
|
2171
|
+
sub.seen.add(item.id);
|
|
2172
|
+
sink.event(item, relayUrl);
|
|
2173
|
+
});
|
|
2174
|
+
}));
|
|
2175
|
+
}
|
|
2176
|
+
function startSubscription(ctx, filters, options, sink) {
|
|
2177
|
+
const live = options?.live ?? true;
|
|
2178
|
+
const strategy = options?.strategy ?? "auto";
|
|
2179
|
+
const authors = deriveAuthors(filters, options?.authors);
|
|
2180
|
+
const sub = { handles: [], seen: /* @__PURE__ */ new Set(), closed: false, eoseCount: 0, relayCount: 0, eoseSent: false };
|
|
2181
|
+
void resolvePlan(ctx, authors, "read", strategy, options?.relays).then((plan) => {
|
|
2182
|
+
if (sub.closed) return;
|
|
2183
|
+
if (plan.relays.length === 0) {
|
|
2184
|
+
sink.closed("relay list unavailable");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
sub.relayCount = plan.relays.length;
|
|
2188
|
+
for (const relayUrl of plan.relays) attachLiveRelay(ctx, sub, filters, relayUrl, live, sink);
|
|
2189
|
+
}).catch((err) => {
|
|
2190
|
+
if (!sub.closed) sink.closed(err instanceof Error ? err.message : "subscribe failed");
|
|
2191
|
+
});
|
|
2192
|
+
return {
|
|
2193
|
+
close() {
|
|
2194
|
+
if (sub.closed) return;
|
|
2195
|
+
sub.closed = true;
|
|
2196
|
+
closeLiveSub(sub);
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
async function resolvePublishTargets(ctx, signed, options) {
|
|
2201
|
+
const strategy = options?.strategy ?? "auto";
|
|
2202
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2203
|
+
const authorPlan = await resolvePlan(ctx, [signed.pubkey], "read", strategy === "inbox" ? "auto" : "outbox");
|
|
2204
|
+
for (const url of authorPlan.relays) targets.add(url);
|
|
2205
|
+
if (options?.targetAuthors && options.targetAuthors.length > 0) {
|
|
2206
|
+
const inboxPlan = await resolvePlan(ctx, options.targetAuthors, "write", "inbox");
|
|
2207
|
+
for (const url of inboxPlan.relays) targets.add(url);
|
|
2208
|
+
}
|
|
2209
|
+
for (const url of options?.relays ?? []) targets.add(url);
|
|
2210
|
+
return allowed(ctx, targets);
|
|
2211
|
+
}
|
|
2212
|
+
async function publishImpl(ctx, template, options) {
|
|
2213
|
+
if (!ctx.signEvent) return { ok: false, error: "publish denied" };
|
|
2214
|
+
if (!ctx.relayPool.isAvailable()) return { ok: false, error: "relay list unavailable" };
|
|
2215
|
+
let signed;
|
|
2216
|
+
try {
|
|
2217
|
+
signed = await ctx.signEvent(template);
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
return { ok: false, error: err instanceof Error ? err.message : "sign failed" };
|
|
2220
|
+
}
|
|
2221
|
+
const relayUrls = await resolvePublishTargets(ctx, signed, options);
|
|
2222
|
+
if (relayUrls.length === 0) return { ok: false, event: signed, eventId: signed.id, error: "relay list unavailable" };
|
|
2223
|
+
let relays;
|
|
2224
|
+
try {
|
|
2225
|
+
relays = normalizePublishResult(await ctx.relayPool.publish(signed, relayUrls), relayUrls);
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
return { ok: false, event: signed, eventId: signed.id, error: err instanceof Error ? err.message : "publish failed" };
|
|
2228
|
+
}
|
|
2229
|
+
const ok = Object.values(relays).some(Boolean);
|
|
2230
|
+
const result = { ok, event: signed, eventId: signed.id, relays };
|
|
2231
|
+
if (!ok) result.error = "publish denied";
|
|
2232
|
+
return result;
|
|
2233
|
+
}
|
|
2234
|
+
function createRelayPoolOutboxRouter(options) {
|
|
2235
|
+
if (!options || typeof options.relayPool !== "object" || options.relayPool === null) {
|
|
2236
|
+
throw new Error("createRelayPoolOutboxRouter: options.relayPool is required");
|
|
2237
|
+
}
|
|
2238
|
+
if (typeof options.loadRelayLists !== "function") {
|
|
2239
|
+
throw new Error("createRelayPoolOutboxRouter: options.loadRelayLists is required");
|
|
2240
|
+
}
|
|
2241
|
+
if (!Array.isArray(options.fallbackRelays)) {
|
|
2242
|
+
throw new Error("createRelayPoolOutboxRouter: options.fallbackRelays is required");
|
|
2243
|
+
}
|
|
2244
|
+
const verifyEvent = options.verifyEvent;
|
|
2245
|
+
const ctx = {
|
|
2246
|
+
relayPool: options.relayPool,
|
|
2247
|
+
loadRelayLists: options.loadRelayLists,
|
|
2248
|
+
fallbackRelays: options.fallbackRelays,
|
|
2249
|
+
signEvent: options.signEvent,
|
|
2250
|
+
isRelayAllowed: options.isRelayAllowed ?? defaultRelayAllowed,
|
|
2251
|
+
defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
2252
|
+
async verify(event) {
|
|
2253
|
+
if (!verifyEvent) return true;
|
|
2254
|
+
try {
|
|
2255
|
+
return await verifyEvent(event);
|
|
2256
|
+
} catch {
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
return {
|
|
2262
|
+
query: (filters, queryOptions) => queryImpl(ctx, filters, queryOptions),
|
|
2263
|
+
subscribe: (filters, subscribeOptions, sink) => startSubscription(ctx, filters, subscribeOptions, sink),
|
|
2264
|
+
publish: (template, publishOptions) => publishImpl(ctx, template, publishOptions),
|
|
2265
|
+
resolveRelays: (target) => {
|
|
2266
|
+
const pubkeys = target.authors ?? (target.pubkey ? [target.pubkey] : []);
|
|
2267
|
+
return resolvePlan(ctx, pubkeys, target.direction ?? "read", target.strategy ?? "auto");
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
function normalizePublishResult(res, relayUrls) {
|
|
2272
|
+
const out = {};
|
|
2273
|
+
if (res && typeof res === "object") {
|
|
2274
|
+
for (const url of relayUrls) out[url] = res[url] ?? false;
|
|
2275
|
+
} else {
|
|
2276
|
+
for (const url of relayUrls) out[url] = true;
|
|
2277
|
+
}
|
|
2278
|
+
return out;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// src/cvm-service.ts
|
|
2282
|
+
var CVM_SERVICE_VERSION = "1.0.0";
|
|
2283
|
+
var CVM_DESCRIPTOR = {
|
|
2284
|
+
name: "cvm",
|
|
2285
|
+
version: CVM_SERVICE_VERSION,
|
|
2286
|
+
description: "NAP-CVM ContextVM bridge \u2014 MCP over Nostr"
|
|
2287
|
+
};
|
|
2288
|
+
function createCvmService(options) {
|
|
2289
|
+
if (!options || typeof options.transport !== "object" || options.transport === null) {
|
|
2290
|
+
throw new Error("createCvmService: options.transport is required");
|
|
2291
|
+
}
|
|
2292
|
+
const { transport } = options;
|
|
2293
|
+
const sendByWindow = /* @__PURE__ */ new Map();
|
|
2294
|
+
const windowsByServer = /* @__PURE__ */ new Map();
|
|
2295
|
+
function openSession(windowId, server, send) {
|
|
2296
|
+
sendByWindow.set(windowId, send);
|
|
2297
|
+
let windows = windowsByServer.get(server.pubkey);
|
|
2298
|
+
if (!windows) {
|
|
2299
|
+
windows = /* @__PURE__ */ new Set();
|
|
2300
|
+
windowsByServer.set(server.pubkey, windows);
|
|
2301
|
+
}
|
|
2302
|
+
windows.add(windowId);
|
|
2303
|
+
}
|
|
2304
|
+
function closeSession(windowId, serverPubkey) {
|
|
2305
|
+
const windows = windowsByServer.get(serverPubkey);
|
|
2306
|
+
if (windows) {
|
|
2307
|
+
windows.delete(windowId);
|
|
2308
|
+
if (windows.size === 0) windowsByServer.delete(serverPubkey);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const eventSub = transport.onEvent((server, message) => {
|
|
2312
|
+
const windows = windowsByServer.get(server.pubkey);
|
|
2313
|
+
if (!windows) return;
|
|
2314
|
+
for (const windowId of windows) {
|
|
2315
|
+
const send = sendByWindow.get(windowId);
|
|
2316
|
+
send?.({ type: "cvm.event", server, message });
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
function handleDiscover(msg, send) {
|
|
2320
|
+
const m = msg;
|
|
2321
|
+
const id = m.id ?? "";
|
|
2322
|
+
void transport.discover(m.query).then((servers) => send({ type: "cvm.discover.result", id, servers })).catch(
|
|
2323
|
+
(err) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage2(err) })
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
function handleRequest(windowId, msg, send) {
|
|
2327
|
+
const m = msg;
|
|
2328
|
+
const id = m.id ?? "";
|
|
2329
|
+
if (!m.server || typeof m.server.pubkey !== "string" || m.server.pubkey.length === 0) {
|
|
2330
|
+
send({ type: "cvm.request.result", id, error: "server not found" });
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
if (!m.message || typeof m.message !== "object") {
|
|
2334
|
+
send({ type: "cvm.request.result", id, error: "unsupported method" });
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
openSession(windowId, m.server, send);
|
|
2338
|
+
void transport.request(m.server, m.message, m.options).then((message) => send({ type: "cvm.request.result", id, message })).catch(
|
|
2339
|
+
(err) => send({ type: "cvm.request.result", id, error: toErrorMessage2(err) })
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
function handleClose(windowId, msg, send) {
|
|
2343
|
+
const m = msg;
|
|
2344
|
+
const id = m.id ?? "";
|
|
2345
|
+
if (!m.server || typeof m.server.pubkey !== "string") {
|
|
2346
|
+
send({ type: "cvm.close.result", id, error: "server not found" });
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
closeSession(windowId, m.server.pubkey);
|
|
2350
|
+
void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err) => send({ type: "cvm.close.result", id, error: toErrorMessage2(err) }));
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
descriptor: CVM_DESCRIPTOR,
|
|
2354
|
+
handleMessage(windowId, message, send) {
|
|
2355
|
+
switch (message.type) {
|
|
2356
|
+
case "cvm.discover":
|
|
2357
|
+
handleDiscover(message, send);
|
|
2358
|
+
return;
|
|
2359
|
+
case "cvm.request":
|
|
2360
|
+
handleRequest(windowId, message, send);
|
|
2361
|
+
return;
|
|
2362
|
+
case "cvm.close":
|
|
2363
|
+
handleClose(windowId, message, send);
|
|
2364
|
+
return;
|
|
2365
|
+
default:
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
},
|
|
2369
|
+
onWindowDestroyed(windowId) {
|
|
2370
|
+
sendByWindow.delete(windowId);
|
|
2371
|
+
for (const [pubkey, windows] of windowsByServer) {
|
|
2372
|
+
windows.delete(windowId);
|
|
2373
|
+
if (windows.size === 0) windowsByServer.delete(pubkey);
|
|
2374
|
+
}
|
|
2375
|
+
},
|
|
2376
|
+
dispose() {
|
|
2377
|
+
eventSub.close();
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
function toErrorMessage2(err) {
|
|
2382
|
+
if (err instanceof Error) return err.message;
|
|
2383
|
+
if (typeof err === "string") return err;
|
|
2384
|
+
return "cvm request failed";
|
|
2385
|
+
}
|
|
873
2386
|
export {
|
|
874
2387
|
createAudioService,
|
|
2388
|
+
createBrowserMediaBridge,
|
|
875
2389
|
createCacheService,
|
|
2390
|
+
createConfigService,
|
|
876
2391
|
createCoordinatedRelay,
|
|
2392
|
+
createCvmService,
|
|
877
2393
|
createIdentityService,
|
|
878
2394
|
createKeysService,
|
|
879
2395
|
createMediaService,
|
|
880
2396
|
createNotificationService,
|
|
881
2397
|
createNotifyService,
|
|
2398
|
+
createOutboxService,
|
|
2399
|
+
createRelayPoolOutboxRouter,
|
|
882
2400
|
createRelayPoolService,
|
|
2401
|
+
createResourceService,
|
|
883
2402
|
createThemeService
|
|
884
2403
|
};
|
|
885
2404
|
//# sourceMappingURL=index.js.map
|