@mh-gg/relay-runtime 0.1.1-alpha.20260613T085325975Z
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/package.json +26 -0
- package/src/encryptedRoomRuntime.cjs +641 -0
- package/src/encryptedRoomRuntimeManager.cjs +131 -0
- package/src/index.cjs +152 -0
- package/src/pluginRuntimeHost.cjs +779 -0
- package/test/encryptedRoomRuntime.test.cjs +729 -0
- package/test/operation-role-keys.test.cjs +346 -0
- package/test/plugin-runtime-manager.test.cjs +651 -0
- package/test/relay-runtime.test.cjs +219 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
const { createEncryptedRoomRuntime, parseStreamHeader } = require("../src/encryptedRoomRuntime.cjs");
|
|
4
|
+
|
|
5
|
+
function hexId(prefix) {
|
|
6
|
+
return String(prefix).padStart(64, "0");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function makeEvent(id, overrides = {}) {
|
|
10
|
+
const eventId = hexId(id);
|
|
11
|
+
return {
|
|
12
|
+
id: eventId,
|
|
13
|
+
pubkey: overrides.pubkey || "pk_" + eventId.slice(0, 8),
|
|
14
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
15
|
+
kind: 9000,
|
|
16
|
+
tags: [
|
|
17
|
+
["scheme", "matterhorn.operation.v2"],
|
|
18
|
+
["d", overrides.roomName || "room"],
|
|
19
|
+
["stream", overrides.stream || "stream:general"],
|
|
20
|
+
["hlc", overrides.hlc || `000000000000001:000000:${overrides.nodeId || "node"}`],
|
|
21
|
+
["member", overrides.member || "alice"],
|
|
22
|
+
["device", overrides.device || "d1"],
|
|
23
|
+
["seq", String(overrides.seq ?? 0)],
|
|
24
|
+
["plugin", overrides.plugin || "chat"],
|
|
25
|
+
["type", overrides.type || "message.send"],
|
|
26
|
+
["role", overrides.role || "member"],
|
|
27
|
+
["epoch", overrides.epoch || "e1"],
|
|
28
|
+
["op-id", overrides.opId || `op_${id}`],
|
|
29
|
+
["payload-hash", overrides.payloadHash || `ph_${id}`]
|
|
30
|
+
],
|
|
31
|
+
content: "encrypted"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("parseStreamHeader extracts v2 header tags", () => {
|
|
36
|
+
const event = makeEvent("e1", { stream: "chat:messages", hlc: "000000000000010:000001:n1", member: "bob", seq: 5 });
|
|
37
|
+
const header = parseStreamHeader(event);
|
|
38
|
+
assert.equal(header.stream, "chat:messages");
|
|
39
|
+
assert.equal(header.hlc, "000000000000010:000001:n1");
|
|
40
|
+
assert.equal(header.writer, "bob");
|
|
41
|
+
assert.equal(header.seq, 5);
|
|
42
|
+
assert.equal(header.plugin, "chat");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("parseStreamHeader rejects invalid HLC", () => {
|
|
46
|
+
const event = makeEvent("e1", { hlc: "not-an-hlc" });
|
|
47
|
+
assert.equal(parseStreamHeader(event), undefined);
|
|
48
|
+
assert.equal(parseStreamHeader({ id: "no-tags" }), undefined);
|
|
49
|
+
assert.equal(parseStreamHeader({ tags: [["hlc", "000000000000001:000000:n1"]] }), undefined);
|
|
50
|
+
|
|
51
|
+
const fallback = parseStreamHeader({
|
|
52
|
+
pubkey: "pubkey-fallback",
|
|
53
|
+
tags: [
|
|
54
|
+
["stream", "s1"],
|
|
55
|
+
["hlc", "000000000000001:000000:n1"],
|
|
56
|
+
["seq", "-1"]
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
assert.equal(fallback.writer, "pubkey-fallback");
|
|
60
|
+
assert.equal(fallback.seq, 0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("insert indexes events by stream", () => {
|
|
64
|
+
const runtime = createEncryptedRoomRuntime();
|
|
65
|
+
const r1 = runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1" }));
|
|
66
|
+
const r2 = runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000002:000000:n1" }));
|
|
67
|
+
const r3 = runtime.insert(makeEvent("e3", { stream: "s2", hlc: "000000000000001:000000:n1" }));
|
|
68
|
+
|
|
69
|
+
assert.equal(r1.ok, true);
|
|
70
|
+
assert.equal(r1.inserted, true);
|
|
71
|
+
assert.equal(r2.ok, true);
|
|
72
|
+
assert.equal(r3.ok, true);
|
|
73
|
+
assert.deepEqual(runtime.listStreams(), ["s1", "s2"]);
|
|
74
|
+
assert.equal(runtime.streamEvents("s1").length, 2);
|
|
75
|
+
assert.equal(runtime.streamEvents("s2").length, 1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("insert deduplicates by event id", () => {
|
|
79
|
+
const runtime = createEncryptedRoomRuntime();
|
|
80
|
+
const ev = makeEvent("e1");
|
|
81
|
+
assert.equal(runtime.insert(ev).inserted, true);
|
|
82
|
+
assert.equal(runtime.insert(ev).inserted, false);
|
|
83
|
+
assert.equal(runtime.stats.duplicates, 1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("insert deduplicates by (stream, hlc, writer, seq)", () => {
|
|
87
|
+
const runtime = createEncryptedRoomRuntime();
|
|
88
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1", member: "alice", seq: 0 }));
|
|
89
|
+
const r2 = runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000001:000000:n1", member: "alice", seq: 0 }));
|
|
90
|
+
assert.equal(r2.inserted, false);
|
|
91
|
+
assert.equal(r2.eventId, hexId("e1"));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("rangeQuery returns events ordered by hlc", () => {
|
|
95
|
+
const runtime = createEncryptedRoomRuntime();
|
|
96
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000003:000000:n1" }));
|
|
97
|
+
runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000001:000000:n1" }));
|
|
98
|
+
runtime.insert(makeEvent("e3", { stream: "s1", hlc: "000000000000002:000000:n1" }));
|
|
99
|
+
|
|
100
|
+
const result = runtime.rangeQuery("s1");
|
|
101
|
+
assert.deepEqual(result.events, [hexId("e2"), hexId("e3"), hexId("e1")]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("rangeQuery respects sinceHlc and untilHlc", () => {
|
|
105
|
+
const runtime = createEncryptedRoomRuntime();
|
|
106
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1" }));
|
|
107
|
+
runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000002:000000:n1" }));
|
|
108
|
+
runtime.insert(makeEvent("e3", { stream: "s1", hlc: "000000000000003:000000:n1" }));
|
|
109
|
+
|
|
110
|
+
const r1 = runtime.rangeQuery("s1", { sinceHlc: "000000000000002:000000:n1" });
|
|
111
|
+
assert.deepEqual(r1.events, [hexId("e2"), hexId("e3")]);
|
|
112
|
+
|
|
113
|
+
const r2 = runtime.rangeQuery("s1", { untilHlc: "000000000000002:000000:n1" });
|
|
114
|
+
assert.deepEqual(r2.events, [hexId("e1"), hexId("e2")]);
|
|
115
|
+
|
|
116
|
+
const r3 = runtime.rangeQuery("s1", { sinceHlc: "000000000000002:000000:n1", untilHlc: "000000000000002:000000:n1" });
|
|
117
|
+
assert.deepEqual(r3.events, [hexId("e2")]);
|
|
118
|
+
|
|
119
|
+
const r4 = runtime.rangeQuery("s1", { beforeHlc: "999999999999999:000000:n1", limit: 2 });
|
|
120
|
+
assert.deepEqual(r4.events, [hexId("e2"), hexId("e3")]);
|
|
121
|
+
|
|
122
|
+
const r5 = runtime.rangeQuery("s1", { sinceHlc: "999999999999999:000000:n1" });
|
|
123
|
+
assert.deepEqual(r5.events, []);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("headQuery returns latest events", () => {
|
|
127
|
+
const runtime = createEncryptedRoomRuntime();
|
|
128
|
+
for (let i = 1; i <= 5; i += 1) {
|
|
129
|
+
runtime.insert(makeEvent(`e${i}`, { stream: "s1", hlc: `00000000000000${i}:000000:n1` }));
|
|
130
|
+
}
|
|
131
|
+
const result = runtime.headQuery("s1", { limit: 2 });
|
|
132
|
+
assert.deepEqual(result.events, [hexId("e4"), hexId("e5")]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("latestHlc returns the newest HLC in a stream", () => {
|
|
136
|
+
const runtime = createEncryptedRoomRuntime();
|
|
137
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1" }));
|
|
138
|
+
runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000003:000000:n1" }));
|
|
139
|
+
runtime.insert(makeEvent("e3", { stream: "s1", hlc: "000000000000002:000000:n1" }));
|
|
140
|
+
assert.equal(runtime.latestHlc("s1"), "000000000000003:000000:n1");
|
|
141
|
+
assert.equal(runtime.latestHlc("unknown"), undefined);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("snapshot and restore round-trip", () => {
|
|
145
|
+
const runtime = createEncryptedRoomRuntime({ roomName: "room-a" });
|
|
146
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1" }));
|
|
147
|
+
runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000002:000000:n1" }));
|
|
148
|
+
|
|
149
|
+
const snap = runtime.snapshot();
|
|
150
|
+
assert.equal(snap.roomName, "room-a");
|
|
151
|
+
assert.equal(snap.events.length, 2);
|
|
152
|
+
|
|
153
|
+
const restored = createEncryptedRoomRuntime();
|
|
154
|
+
const result = restored.restore(snap);
|
|
155
|
+
assert.equal(result.ok, true);
|
|
156
|
+
assert.equal(result.inserted, 2);
|
|
157
|
+
assert.deepEqual(restored.listStreams(), ["s1"]);
|
|
158
|
+
assert.deepEqual(restored.rangeQuery("s1").events, [hexId("e1"), hexId("e2")]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("eviction keeps maxEventsPerStream", () => {
|
|
162
|
+
const runtime = createEncryptedRoomRuntime({ maxEventsPerStream: 3 });
|
|
163
|
+
for (let i = 1; i <= 5; i += 1) {
|
|
164
|
+
runtime.insert(makeEvent(`e${i}`, { stream: "s1", hlc: `00000000000000${i}:000000:n1` }));
|
|
165
|
+
}
|
|
166
|
+
assert.equal(runtime.streamEvents("s1").length, 3);
|
|
167
|
+
assert.equal(runtime.stats.evictions, 2);
|
|
168
|
+
assert.deepEqual(runtime.rangeQuery("s1").events, [hexId("e3"), hexId("e4"), hexId("e5")]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("createNegentropySession returns a session scoped to stream", () => {
|
|
172
|
+
const runtime = createEncryptedRoomRuntime();
|
|
173
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1", created_at: 1000 }));
|
|
174
|
+
runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000002:000000:n1", created_at: 1001 }));
|
|
175
|
+
runtime.insert(makeEvent("e3", { stream: "s2", hlc: "000000000000001:000000:n1", created_at: 1002 }));
|
|
176
|
+
|
|
177
|
+
const session = runtime.createNegentropySession("s1");
|
|
178
|
+
assert.ok(session.negentropy);
|
|
179
|
+
assert.equal(typeof session.negentropy.initiate(), "string");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("large thread: latest 50 plus chunked scroll back", () => {
|
|
183
|
+
const runtime = createEncryptedRoomRuntime();
|
|
184
|
+
const stream = "chat:messages:general";
|
|
185
|
+
const total = 600;
|
|
186
|
+
for (let i = 1; i <= total; i += 1) {
|
|
187
|
+
const physical = String(i).padStart(15, "0");
|
|
188
|
+
runtime.insert(makeEvent(`m${i}`, {
|
|
189
|
+
stream,
|
|
190
|
+
hlc: `${physical}:000000:n1`,
|
|
191
|
+
member: i % 3 === 0 ? "bob" : "alice",
|
|
192
|
+
seq: i - 1,
|
|
193
|
+
created_at: 1000 + i
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 1. Client opens thread: fetch latest 50 messages.
|
|
198
|
+
const latest = runtime.headQuery(stream, { limit: 50 });
|
|
199
|
+
assert.equal(latest.events.length, 50);
|
|
200
|
+
assert.equal(latest.events[0], hexId("m551"));
|
|
201
|
+
assert.equal(latest.events[latest.events.length - 1], hexId("m600"));
|
|
202
|
+
|
|
203
|
+
// 2. Scroll up: fetch previous 50 messages (messages 501-550).
|
|
204
|
+
const oldestLoadedHlc = "000000000000551:000000:n1";
|
|
205
|
+
const chunk1 = runtime.rangeQuery(stream, { beforeHlc: oldestLoadedHlc, limit: 50 });
|
|
206
|
+
assert.equal(chunk1.events.length, 50);
|
|
207
|
+
assert.equal(chunk1.events[0], hexId("m501"));
|
|
208
|
+
assert.equal(chunk1.events[chunk1.events.length - 1], hexId("m550"));
|
|
209
|
+
|
|
210
|
+
// 3. Scroll up again: fetch next 50 older messages (messages 451-500).
|
|
211
|
+
const nextOldestHlc = "000000000000501:000000:n1";
|
|
212
|
+
const chunk2 = runtime.rangeQuery(stream, { beforeHlc: nextOldestHlc, limit: 50 });
|
|
213
|
+
assert.equal(chunk2.events.length, 50);
|
|
214
|
+
assert.equal(chunk2.events[0], hexId("m451"));
|
|
215
|
+
assert.equal(chunk2.events[chunk2.events.length - 1], hexId("m500"));
|
|
216
|
+
|
|
217
|
+
// 4. Jump to a specific historical range (messages 101-150, inclusive).
|
|
218
|
+
const range = runtime.rangeQuery(stream, {
|
|
219
|
+
sinceHlc: "000000000000101:000000:n1",
|
|
220
|
+
untilHlc: "000000000000150:000000:n1"
|
|
221
|
+
});
|
|
222
|
+
assert.equal(range.events.length, 50);
|
|
223
|
+
assert.equal(range.events[0], hexId("m101"));
|
|
224
|
+
assert.equal(range.events[range.events.length - 1], hexId("m150"));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("large thread: query totals and pagination offsets", () => {
|
|
228
|
+
const runtime = createEncryptedRoomRuntime();
|
|
229
|
+
const stream = "chat:messages:general";
|
|
230
|
+
const total = 600;
|
|
231
|
+
for (let i = 1; i <= total; i += 1) {
|
|
232
|
+
const physical = String(i).padStart(15, "0");
|
|
233
|
+
runtime.insert(makeEvent(`m${i}`, {
|
|
234
|
+
stream,
|
|
235
|
+
hlc: `${physical}:000000:n1`,
|
|
236
|
+
seq: i - 1
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const pageSize = 100;
|
|
241
|
+
const firstPage = runtime.rangeQuery(stream, { limit: pageSize, offset: 0 });
|
|
242
|
+
assert.equal(firstPage.total, 600);
|
|
243
|
+
assert.equal(firstPage.events.length, 100);
|
|
244
|
+
assert.equal(firstPage.events[0], hexId("m1"));
|
|
245
|
+
|
|
246
|
+
const secondPage = runtime.rangeQuery(stream, { limit: pageSize, offset: 100 });
|
|
247
|
+
assert.equal(secondPage.events.length, 100);
|
|
248
|
+
assert.equal(secondPage.events[0], hexId("m101"));
|
|
249
|
+
|
|
250
|
+
const lastPage = runtime.rangeQuery(stream, { limit: pageSize, offset: 500 });
|
|
251
|
+
assert.equal(lastPage.events.length, 100);
|
|
252
|
+
assert.equal(lastPage.events[0], hexId("m501"));
|
|
253
|
+
assert.equal(lastPage.events[lastPage.events.length - 1], hexId("m600"));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("snapshot and restore preserves large thread queries without room secrets", () => {
|
|
257
|
+
const runtime = createEncryptedRoomRuntime({ roomName: "encrypted-room" });
|
|
258
|
+
const stream = "chat:messages:thread-a";
|
|
259
|
+
const total = 600;
|
|
260
|
+
for (let i = 1; i <= total; i += 1) {
|
|
261
|
+
const physical = String(i).padStart(15, "0");
|
|
262
|
+
runtime.insert(makeEvent(`m${i}`, {
|
|
263
|
+
stream,
|
|
264
|
+
hlc: `${physical}:000000:n1`,
|
|
265
|
+
member: i % 2 === 0 ? "bob" : "alice",
|
|
266
|
+
seq: i - 1,
|
|
267
|
+
created_at: 2000 + i
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Snapshot the runtime state. This simulates relay shutdown persistence.
|
|
272
|
+
const snap = runtime.snapshot();
|
|
273
|
+
assert.equal(snap.events.length, 600);
|
|
274
|
+
assert.equal(snap.roomName, "encrypted-room");
|
|
275
|
+
// No room secret is ever part of the snapshot.
|
|
276
|
+
assert.equal(snap.roomSecret, undefined);
|
|
277
|
+
assert.equal(snap.epochKey, undefined);
|
|
278
|
+
|
|
279
|
+
// Create a fresh runtime and restore. This simulates relay restart.
|
|
280
|
+
const restored = createEncryptedRoomRuntime({ roomName: "encrypted-room" });
|
|
281
|
+
const restoreResult = restored.restore(snap);
|
|
282
|
+
assert.equal(restoreResult.ok, true);
|
|
283
|
+
assert.equal(restoreResult.inserted, 600);
|
|
284
|
+
|
|
285
|
+
// Latest query still works.
|
|
286
|
+
const latest = restored.headQuery(stream, { limit: 50 });
|
|
287
|
+
assert.equal(latest.events.length, 50);
|
|
288
|
+
assert.equal(latest.events[0], hexId("m551"));
|
|
289
|
+
assert.equal(latest.events[latest.events.length - 1], hexId("m600"));
|
|
290
|
+
|
|
291
|
+
// Chunked scroll back still works.
|
|
292
|
+
const chunk = restored.rangeQuery(stream, { beforeHlc: "000000000000551:000000:n1", limit: 50 });
|
|
293
|
+
assert.equal(chunk.events.length, 50);
|
|
294
|
+
assert.equal(chunk.events[0], hexId("m501"));
|
|
295
|
+
assert.equal(chunk.events[chunk.events.length - 1], hexId("m550"));
|
|
296
|
+
|
|
297
|
+
// Range jump still works.
|
|
298
|
+
const range = restored.rangeQuery(stream, {
|
|
299
|
+
sinceHlc: "000000000000101:000000:n1",
|
|
300
|
+
untilHlc: "000000000000150:000000:n1"
|
|
301
|
+
});
|
|
302
|
+
assert.equal(range.events.length, 50);
|
|
303
|
+
assert.equal(range.events[0], hexId("m101"));
|
|
304
|
+
|
|
305
|
+
// Stream metadata is preserved.
|
|
306
|
+
assert.deepEqual(restored.listStreams(), [stream]);
|
|
307
|
+
assert.equal(restored.latestHlc(stream), "000000000000600:000000:n1");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("restore ignores duplicate events across restart", () => {
|
|
311
|
+
const runtime = createEncryptedRoomRuntime();
|
|
312
|
+
const stream = "chat:messages:general";
|
|
313
|
+
runtime.insert(makeEvent("e1", { stream, hlc: "000000000000001:000000:n1" }));
|
|
314
|
+
runtime.insert(makeEvent("e2", { stream, hlc: "000000000000002:000000:n1" }));
|
|
315
|
+
|
|
316
|
+
const snap = runtime.snapshot();
|
|
317
|
+
const restored = createEncryptedRoomRuntime();
|
|
318
|
+
restored.restore(snap);
|
|
319
|
+
|
|
320
|
+
// Simulate the same events arriving again after restart (e.g. mesh replay).
|
|
321
|
+
const replay1 = restored.insert(makeEvent("e1", { stream, hlc: "000000000000001:000000:n1" }));
|
|
322
|
+
const replay2 = restored.insert(makeEvent("e2", { stream, hlc: "000000000000002:000000:n1" }));
|
|
323
|
+
assert.equal(replay1.inserted, false);
|
|
324
|
+
assert.equal(replay2.inserted, false);
|
|
325
|
+
assert.equal(restored.streamEvents(stream).length, 2);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("restore with empty snapshot is a no-op", () => {
|
|
329
|
+
const runtime = createEncryptedRoomRuntime();
|
|
330
|
+
const result = runtime.restore({ events: [] });
|
|
331
|
+
assert.equal(result.ok, true);
|
|
332
|
+
assert.equal(result.inserted, 0);
|
|
333
|
+
assert.deepEqual(runtime.listStreams(), []);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("restore rejects invalid snapshot", () => {
|
|
337
|
+
const runtime = createEncryptedRoomRuntime();
|
|
338
|
+
const result = runtime.restore({ notEvents: [] });
|
|
339
|
+
assert.equal(result.ok, false);
|
|
340
|
+
assert.equal(result.reason, "invalid-snapshot");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("insert rejects missing event id and missing stream header", () => {
|
|
344
|
+
const runtime = createEncryptedRoomRuntime();
|
|
345
|
+
assert.equal(runtime.insert({ kind: 9001 }).ok, false);
|
|
346
|
+
assert.equal(runtime.insert({ id: "abc", kind: 9999 }).ok, false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("has and get expose stored events", () => {
|
|
350
|
+
const runtime = createEncryptedRoomRuntime();
|
|
351
|
+
const ev = makeEvent("e1");
|
|
352
|
+
runtime.insert(ev);
|
|
353
|
+
assert.equal(runtime.has(hexId("e1")), true);
|
|
354
|
+
assert.equal(runtime.has("missing"), false);
|
|
355
|
+
assert.equal(runtime.get(hexId("e1"))?.id, hexId("e1"));
|
|
356
|
+
assert.equal(runtime.get("missing"), undefined);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("streamWritersFor lists distinct writers", () => {
|
|
360
|
+
const runtime = createEncryptedRoomRuntime();
|
|
361
|
+
runtime.insert(makeEvent("e1", { stream: "s1", member: "alice" }));
|
|
362
|
+
runtime.insert(makeEvent("e2", { stream: "s1", member: "alice" }));
|
|
363
|
+
runtime.insert(makeEvent("e3", { stream: "s1", member: "bob" }));
|
|
364
|
+
assert.deepEqual(runtime.streamWritersFor("s1"), ["alice", "bob"]);
|
|
365
|
+
assert.deepEqual(runtime.streamWritersFor("unknown"), []);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("deduplicates by (stream, hlc, writer, seq) across writers and seq", () => {
|
|
369
|
+
const runtime = createEncryptedRoomRuntime();
|
|
370
|
+
runtime.insert(makeEvent("e1", { stream: "s1", hlc: "000000000000001:000000:n1", member: "alice", seq: 0 }));
|
|
371
|
+
runtime.insert(makeEvent("e2", { stream: "s1", hlc: "000000000000001:000000:n1", member: "bob", seq: 0 }));
|
|
372
|
+
runtime.insert(makeEvent("e3", { stream: "s1", hlc: "000000000000001:000000:n1", member: "alice", seq: 1 }));
|
|
373
|
+
assert.equal(runtime.streamEvents("s1").length, 3);
|
|
374
|
+
|
|
375
|
+
const dup = runtime.insert(makeEvent("e4", { stream: "s1", hlc: "000000000000001:000000:n1", member: "alice", seq: 0 }));
|
|
376
|
+
assert.equal(dup.inserted, false);
|
|
377
|
+
assert.equal(dup.eventId, hexId("e1"));
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("control-plane parse tolerates malformed events", () => {
|
|
381
|
+
const runtime = createEncryptedRoomRuntime();
|
|
382
|
+
const bad = {
|
|
383
|
+
id: hexId("bad"),
|
|
384
|
+
pubkey: "pk",
|
|
385
|
+
created_at: 1000,
|
|
386
|
+
kind: 9010,
|
|
387
|
+
tags: [["protocol", "matterhorn-sdk"], ["version", "1"], ["d", "room"]],
|
|
388
|
+
content: "not-json"
|
|
389
|
+
};
|
|
390
|
+
const result = runtime.insert(bad);
|
|
391
|
+
assert.equal(result.ok, true);
|
|
392
|
+
assert.equal(result.inserted, true);
|
|
393
|
+
assert.equal(runtime.activeKeyEpoch(), undefined);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("control-plane retirement without a successor leaves no active epoch", () => {
|
|
397
|
+
const runtime = createEncryptedRoomRuntime();
|
|
398
|
+
runtime.insert(makeControlPlaneEvent("ke1", 9010, {
|
|
399
|
+
kind: "room.key.epoch",
|
|
400
|
+
version: 1,
|
|
401
|
+
id: "epoch_1",
|
|
402
|
+
index: 1,
|
|
403
|
+
createdAt: 1000,
|
|
404
|
+
createdBy: "admin",
|
|
405
|
+
retiredAt: 2000,
|
|
406
|
+
historyVisibility: "retained-keyring"
|
|
407
|
+
}));
|
|
408
|
+
assert.equal(runtime.activeKeyEpoch(), undefined);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
function makeControlPlaneEvent(id, kind, payload, overrides = {}) {
|
|
412
|
+
const eventId = hexId(id);
|
|
413
|
+
return {
|
|
414
|
+
id: eventId,
|
|
415
|
+
pubkey: overrides.pubkey || "pk_admin",
|
|
416
|
+
created_at: overrides.createdAt ?? 1000,
|
|
417
|
+
kind,
|
|
418
|
+
tags: [
|
|
419
|
+
["protocol", "matterhorn-sdk"],
|
|
420
|
+
["version", "1"],
|
|
421
|
+
["d", overrides.roomName || "room"]
|
|
422
|
+
],
|
|
423
|
+
content: JSON.stringify(payload)
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
test("folds room.key.epoch into active key epoch view", () => {
|
|
428
|
+
const runtime = createEncryptedRoomRuntime();
|
|
429
|
+
const epoch = makeControlPlaneEvent("ke1", 9010, {
|
|
430
|
+
kind: "room.key.epoch",
|
|
431
|
+
version: 1,
|
|
432
|
+
id: "epoch_1",
|
|
433
|
+
index: 1,
|
|
434
|
+
createdAt: 1000,
|
|
435
|
+
createdBy: "admin",
|
|
436
|
+
createdByDevice: "dev-admin",
|
|
437
|
+
historyVisibility: "retained-keyring"
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const result = runtime.insert(epoch);
|
|
441
|
+
assert.equal(result.ok, true);
|
|
442
|
+
assert.equal(result.inserted, true);
|
|
443
|
+
|
|
444
|
+
const active = runtime.activeKeyEpoch();
|
|
445
|
+
assert.ok(active);
|
|
446
|
+
assert.equal(active.id, "epoch_1");
|
|
447
|
+
assert.equal(active.index, 1);
|
|
448
|
+
assert.equal(active.historyVisibility, "retained-keyring");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("retires previous active epoch when a new epoch is created", () => {
|
|
452
|
+
const runtime = createEncryptedRoomRuntime();
|
|
453
|
+
runtime.insert(makeControlPlaneEvent("ke1", 9010, {
|
|
454
|
+
kind: "room.key.epoch",
|
|
455
|
+
version: 1,
|
|
456
|
+
id: "epoch_1",
|
|
457
|
+
index: 1,
|
|
458
|
+
createdAt: 1000,
|
|
459
|
+
createdBy: "admin",
|
|
460
|
+
historyVisibility: "retained-keyring"
|
|
461
|
+
}));
|
|
462
|
+
|
|
463
|
+
runtime.insert(makeControlPlaneEvent("ke2", 9010, {
|
|
464
|
+
kind: "room.key.epoch",
|
|
465
|
+
version: 1,
|
|
466
|
+
id: "epoch_2",
|
|
467
|
+
index: 2,
|
|
468
|
+
createdAt: 2000,
|
|
469
|
+
createdBy: "admin",
|
|
470
|
+
historyVisibility: "active-epoch-only"
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
runtime.insert(makeControlPlaneEvent("ke1r", 9010, {
|
|
474
|
+
kind: "room.key.epoch",
|
|
475
|
+
version: 1,
|
|
476
|
+
id: "epoch_1",
|
|
477
|
+
index: 1,
|
|
478
|
+
createdAt: 1000,
|
|
479
|
+
createdBy: "admin",
|
|
480
|
+
retiredAt: 2000,
|
|
481
|
+
historyVisibility: "retained-keyring"
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
const active = runtime.activeKeyEpoch();
|
|
485
|
+
assert.equal(active.id, "epoch_2");
|
|
486
|
+
assert.equal(active.historyVisibility, "active-epoch-only");
|
|
487
|
+
|
|
488
|
+
const epochs = runtime.listKeyEpochs();
|
|
489
|
+
assert.equal(epochs.length, 2);
|
|
490
|
+
const retired = epochs.find((e) => e.id === "epoch_1");
|
|
491
|
+
assert.equal(retired.retiredAt, 2000);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("folds key epoch grants and room index key grants", () => {
|
|
495
|
+
const runtime = createEncryptedRoomRuntime();
|
|
496
|
+
|
|
497
|
+
runtime.insert(makeControlPlaneEvent("keg1", 9011, {
|
|
498
|
+
kind: "matterhorn.key-epoch-grant",
|
|
499
|
+
version: 1,
|
|
500
|
+
roomName: "room",
|
|
501
|
+
epochId: "epoch_1",
|
|
502
|
+
epochIndex: 1,
|
|
503
|
+
recipientMemberId: "alice",
|
|
504
|
+
recipientDeviceId: "dev-a",
|
|
505
|
+
recipientEncryptionKeyId: "rkx_alice",
|
|
506
|
+
createdAt: 1000,
|
|
507
|
+
wrap: { alg: "x25519+hkdf-sha256+aes-256-gcm", ephemeralPublicKey: "epk", iv: "iv" },
|
|
508
|
+
wrappedEpochKey: "cipher"
|
|
509
|
+
}));
|
|
510
|
+
|
|
511
|
+
runtime.insert(makeControlPlaneEvent("kig1", 9012, {
|
|
512
|
+
kind: "matterhorn.room-index-key-grant",
|
|
513
|
+
version: 1,
|
|
514
|
+
recipientMemberId: "alice",
|
|
515
|
+
recipientDeviceId: "dev-a",
|
|
516
|
+
recipientEncryptionKeyId: "rkx_alice",
|
|
517
|
+
createdAt: 1000,
|
|
518
|
+
wrap: { alg: "x25519+hkdf-sha256+aes-256-gcm", ephemeralPublicKey: "epk", iv: "iv" },
|
|
519
|
+
wrappedRoomIndexKey: "cipher"
|
|
520
|
+
}));
|
|
521
|
+
|
|
522
|
+
assert.deepEqual(runtime.listEpochGrantRecipients("epoch_1"), ["rkx_alice"]);
|
|
523
|
+
assert.deepEqual(runtime.listRoomIndexKeyGrantRecipients(), ["rkx_alice"]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("keyEpochForIngress returns active epoch when epochId is omitted", () => {
|
|
527
|
+
const runtime = createEncryptedRoomRuntime();
|
|
528
|
+
runtime.insert(makeControlPlaneEvent("ke1", 9010, {
|
|
529
|
+
kind: "room.key.epoch",
|
|
530
|
+
version: 1,
|
|
531
|
+
id: "epoch_1",
|
|
532
|
+
index: 1,
|
|
533
|
+
createdAt: 1000,
|
|
534
|
+
createdBy: "admin",
|
|
535
|
+
historyVisibility: "retained-keyring"
|
|
536
|
+
}));
|
|
537
|
+
|
|
538
|
+
assert.equal(runtime.keyEpochForIngress().id, "epoch_1");
|
|
539
|
+
assert.equal(runtime.keyEpochForIngress("epoch_1").id, "epoch_1");
|
|
540
|
+
assert.equal(runtime.keyEpochForIngress("missing"), undefined);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("snapshot and restore preserves control-plane state", () => {
|
|
544
|
+
const runtime = createEncryptedRoomRuntime();
|
|
545
|
+
runtime.insert(makeControlPlaneEvent("ke1", 9010, {
|
|
546
|
+
kind: "room.key.epoch",
|
|
547
|
+
version: 1,
|
|
548
|
+
id: "epoch_1",
|
|
549
|
+
index: 1,
|
|
550
|
+
createdAt: 1000,
|
|
551
|
+
createdBy: "admin",
|
|
552
|
+
historyVisibility: "retained-keyring"
|
|
553
|
+
}));
|
|
554
|
+
runtime.insert(makeControlPlaneEvent("keg1", 9011, {
|
|
555
|
+
kind: "matterhorn.key-epoch-grant",
|
|
556
|
+
version: 1,
|
|
557
|
+
roomName: "room",
|
|
558
|
+
epochId: "epoch_1",
|
|
559
|
+
epochIndex: 1,
|
|
560
|
+
recipientMemberId: "alice",
|
|
561
|
+
recipientDeviceId: "dev-a",
|
|
562
|
+
recipientEncryptionKeyId: "rkx_alice",
|
|
563
|
+
createdAt: 1000,
|
|
564
|
+
wrap: { alg: "x25519+hkdf-sha256+aes-256-gcm", ephemeralPublicKey: "epk", iv: "iv" },
|
|
565
|
+
wrappedEpochKey: "cipher"
|
|
566
|
+
}));
|
|
567
|
+
|
|
568
|
+
const snap = runtime.snapshot();
|
|
569
|
+
const restored = createEncryptedRoomRuntime();
|
|
570
|
+
restored.restore(snap);
|
|
571
|
+
|
|
572
|
+
assert.equal(restored.activeKeyEpoch().id, "epoch_1");
|
|
573
|
+
assert.deepEqual(restored.listEpochGrantRecipients("epoch_1"), ["rkx_alice"]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const {
|
|
577
|
+
createRoomIndexNgramEvents,
|
|
578
|
+
generatePartyIdentity,
|
|
579
|
+
roomIndexSearchQuery
|
|
580
|
+
} = require("@mh-gg/event");
|
|
581
|
+
|
|
582
|
+
function makeNgramIndexEvent(id, payload, overrides = {}) {
|
|
583
|
+
return {
|
|
584
|
+
id: hexId(id),
|
|
585
|
+
pubkey: overrides.pubkey || "pk_index",
|
|
586
|
+
created_at: overrides.createdAt ?? 1700000000,
|
|
587
|
+
kind: 9013,
|
|
588
|
+
tags: overrides.tags || [["d", payload.roomName || "room"]],
|
|
589
|
+
content: JSON.stringify(payload)
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
test("server-side n-gram search indexes opaque sidecar tokens", () => {
|
|
594
|
+
const roomName = "room";
|
|
595
|
+
const roomIndexKey = "22".repeat(32);
|
|
596
|
+
const identity = generatePartyIdentity(1700000000000);
|
|
597
|
+
const runtime = createEncryptedRoomRuntime({ roomName });
|
|
598
|
+
const target = makeEvent("e9", {
|
|
599
|
+
roomName,
|
|
600
|
+
stream: "chat:messages:general",
|
|
601
|
+
hlc: "000000000000009:000000:n1",
|
|
602
|
+
opId: "op_e9"
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
assert.equal(runtime.insert(target).inserted, true);
|
|
606
|
+
const indexEvents = createRoomIndexNgramEvents({
|
|
607
|
+
identity,
|
|
608
|
+
roomIndexKey,
|
|
609
|
+
roomName,
|
|
610
|
+
targetEventId: target.id,
|
|
611
|
+
targetOperationId: "op_e9",
|
|
612
|
+
targetHlc: "000000000000009:000000:n1",
|
|
613
|
+
stream: "chat:messages:general",
|
|
614
|
+
field: "body",
|
|
615
|
+
text: "Deploy launch codes in alpha channel",
|
|
616
|
+
createdAt: 1700000000000,
|
|
617
|
+
maxTokensPerEvent: 512
|
|
618
|
+
});
|
|
619
|
+
assert.equal(indexEvents[0].content.includes("launch"), false);
|
|
620
|
+
assert.equal(indexEvents[0].content.includes("codes"), false);
|
|
621
|
+
for (const event of indexEvents) assert.equal(runtime.insert(event).inserted, true);
|
|
622
|
+
|
|
623
|
+
const query = roomIndexSearchQuery({ roomIndexKey, roomName, text: "launch codes" });
|
|
624
|
+
const result = runtime.searchNgrams({ ...query, stream: "chat:messages:general" });
|
|
625
|
+
assert.deepEqual(result.events, [target.id]);
|
|
626
|
+
assert.equal(result.matches[0].targetOperationId, "op_e9");
|
|
627
|
+
assert.equal(result.matches[0].field, "body");
|
|
628
|
+
|
|
629
|
+
const miss = roomIndexSearchQuery({ roomIndexKey, roomName, text: "unrelated" });
|
|
630
|
+
assert.deepEqual(runtime.searchNgrams(miss).events, []);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("server-side n-gram search covers modes, invalid tokens, and pruning", () => {
|
|
634
|
+
const runtime = createEncryptedRoomRuntime({ roomName: "room", maxEventsPerStream: 2 });
|
|
635
|
+
const targetA = makeEvent("a1", {
|
|
636
|
+
roomName: "room",
|
|
637
|
+
stream: "chat:messages:general",
|
|
638
|
+
hlc: "000000000000001:000000:n1",
|
|
639
|
+
opId: "op_a1"
|
|
640
|
+
});
|
|
641
|
+
const targetB = makeEvent("b1", {
|
|
642
|
+
roomName: "room",
|
|
643
|
+
stream: "chat:messages:general",
|
|
644
|
+
hlc: "000000000000002:000000:n1",
|
|
645
|
+
opId: "op_b1"
|
|
646
|
+
});
|
|
647
|
+
const tokenA = "abcdefghijklmnopqrstuvwxyz";
|
|
648
|
+
const tokenB = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
649
|
+
|
|
650
|
+
runtime.insert(targetA);
|
|
651
|
+
runtime.insert(makeNgramIndexEvent("i1", {
|
|
652
|
+
kind: "matterhorn.room-index-ngram",
|
|
653
|
+
version: 1,
|
|
654
|
+
suite: "matterhorn.ngram.hmac-sha256.v1",
|
|
655
|
+
roomName: "room",
|
|
656
|
+
keyId: "key",
|
|
657
|
+
targetEventId: targetA.id,
|
|
658
|
+
targetOperationId: "op_a1",
|
|
659
|
+
targetHlc: "000000000000001:000000:n1",
|
|
660
|
+
stream: "chat:messages:general",
|
|
661
|
+
field: "body",
|
|
662
|
+
tokenCount: 2,
|
|
663
|
+
createdAt: 10,
|
|
664
|
+
tokens: [tokenA, tokenB, "bad"]
|
|
665
|
+
}));
|
|
666
|
+
runtime.insert(makeNgramIndexEvent("i1", {
|
|
667
|
+
kind: "matterhorn.room-index-ngram",
|
|
668
|
+
version: 1,
|
|
669
|
+
suite: "matterhorn.ngram.hmac-sha256.v1",
|
|
670
|
+
roomName: "room",
|
|
671
|
+
keyId: "key",
|
|
672
|
+
targetEventId: targetA.id,
|
|
673
|
+
tokens: [tokenA]
|
|
674
|
+
}));
|
|
675
|
+
runtime.insert(targetB);
|
|
676
|
+
runtime.insert(makeNgramIndexEvent("i2", {
|
|
677
|
+
kind: "matterhorn.room-index-ngram",
|
|
678
|
+
version: 1,
|
|
679
|
+
suite: "matterhorn.ngram.hmac-sha256.v1",
|
|
680
|
+
roomName: "room",
|
|
681
|
+
keyId: "key",
|
|
682
|
+
targetEventId: targetB.id,
|
|
683
|
+
targetHlc: "000000000000002:000000:n1",
|
|
684
|
+
stream: "chat:messages:general",
|
|
685
|
+
tokenCount: 1,
|
|
686
|
+
createdAt: 20,
|
|
687
|
+
tokens: [tokenA]
|
|
688
|
+
}));
|
|
689
|
+
|
|
690
|
+
assert.deepEqual(runtime.searchNgrams({ keyId: "key", tokens: ["bad"] }), { events: [], matches: [], total: 0, queryTokenCount: 0 });
|
|
691
|
+
const anyResult = runtime.searchNgrams({ keyId: "key", tokens: [tokenA, tokenB], mode: "any", order: "score", limit: 1 });
|
|
692
|
+
assert.equal(anyResult.events.length, 1);
|
|
693
|
+
assert.equal(anyResult.queryTokenCount, 2);
|
|
694
|
+
|
|
695
|
+
const thresholdResult = runtime.searchNgrams({ keyId: "key", tokens: [tokenA, tokenB], mode: "threshold", minScore: 0.75 });
|
|
696
|
+
assert.equal(thresholdResult.events.includes(targetA.id), true);
|
|
697
|
+
assert.equal(thresholdResult.events.includes(targetB.id), false);
|
|
698
|
+
|
|
699
|
+
assert.deepEqual(runtime.searchNgrams({ keyId: "key", tokens: [tokenA], stream: "other" }).events, []);
|
|
700
|
+
|
|
701
|
+
runtime.insert(makeEvent("c1", { roomName: "room", stream: "chat:messages:general", hlc: "000000000000003:000000:n1" }));
|
|
702
|
+
assert.equal(runtime.has(targetA.id), false);
|
|
703
|
+
assert.equal(runtime.searchNgrams({ keyId: "key", tokens: [tokenB] }).events.includes(targetA.id), false);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("server-side n-gram index ignores malformed sidecars without leaking state", () => {
|
|
707
|
+
const runtime = createEncryptedRoomRuntime({ roomName: "room" });
|
|
708
|
+
const target = makeEvent("d1", { roomName: "room" });
|
|
709
|
+
runtime.insert(target);
|
|
710
|
+
|
|
711
|
+
const malformedPayloads = [
|
|
712
|
+
null,
|
|
713
|
+
{ kind: "matterhorn.room-index-ngram", version: 1, suite: "bad", roomName: "room", keyId: "key", targetEventId: target.id, tokens: ["abcdefghijklmnopqrstuvwxyz"] },
|
|
714
|
+
{ kind: "matterhorn.room-index-ngram", version: 1, suite: "matterhorn.ngram.hmac-sha256.v1", roomName: "other", keyId: "key", targetEventId: target.id, tokens: ["abcdefghijklmnopqrstuvwxyz"] },
|
|
715
|
+
{ kind: "matterhorn.room-index-ngram", version: 1, suite: "matterhorn.ngram.hmac-sha256.v1", roomName: "room", keyId: "", targetEventId: target.id, tokens: ["abcdefghijklmnopqrstuvwxyz"] },
|
|
716
|
+
{ kind: "matterhorn.room-index-ngram", version: 1, suite: "matterhorn.ngram.hmac-sha256.v1", roomName: "room", keyId: "key", targetEventId: "bad", tokens: ["abcdefghijklmnopqrstuvwxyz"] },
|
|
717
|
+
{ kind: "matterhorn.room-index-ngram", version: 1, suite: "matterhorn.ngram.hmac-sha256.v1", roomName: "room", keyId: "key", targetEventId: target.id, tokens: [] },
|
|
718
|
+
{ kind: "matterhorn.room-index-ngram", version: 1, suite: "matterhorn.ngram.hmac-sha256.v1", roomName: "room", keyId: "key", targetEventId: target.id, tokens: ["bad"] }
|
|
719
|
+
];
|
|
720
|
+
|
|
721
|
+
for (const [index, payload] of malformedPayloads.entries()) {
|
|
722
|
+
const event = payload
|
|
723
|
+
? makeNgramIndexEvent(`m${index}`, payload)
|
|
724
|
+
: { ...makeNgramIndexEvent(`m${index}`, {}, {}), content: "not-json" };
|
|
725
|
+
assert.equal(runtime.insert(event).ok, true);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
assert.deepEqual(runtime.ngramIndexStats(), { targets: 0, indexEvents: 0, tokenPostingLists: 0, postings: 0 });
|
|
729
|
+
});
|