@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.
@@ -0,0 +1,346 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+
4
+ const { manifestHash } = require("@mh-gg/base");
5
+ const {
6
+ createOperationGrantAuthority,
7
+ createOperationRoleKeyGrant,
8
+ generateOperationGrantAuthorityKeyPair,
9
+ generateOperationRoleKeyPair,
10
+ signOperationRoleKeyGrant,
11
+ signOperationWithRoleKey
12
+ } = require("@mh-gg/host-runtime");
13
+ const { createRelayPluginRuntimeManager } = require("../src/index.cjs");
14
+ const { kanbanAppPack, kanbanHostPlugins, KANBAN_PLUGIN_ID } = require("../../../examples/kanban/src/sdk-app.mjs");
15
+
16
+ const ROOM_ID = "room_relay_role_keys";
17
+ const APP_HASH = manifestHash(kanbanAppPack);
18
+
19
+ function actor(role = "member", memberId = "member") {
20
+ return { memberId, deviceId: `dev_${memberId}`, role, displayName: memberId };
21
+ }
22
+
23
+ function operation(overrides = {}) {
24
+ return {
25
+ clientOperationId: overrides.id || "op_1",
26
+ roomId: ROOM_ID,
27
+ appPackId: kanbanAppPack.id,
28
+ appPackHash: APP_HASH,
29
+ pluginId: overrides.pluginId || KANBAN_PLUGIN_ID,
30
+ type: overrides.type || "list.create",
31
+ actor: overrides.actor || actor("admin", "alice"),
32
+ seq: overrides.seq || 1,
33
+ createdAt: overrides.createdAt || 1000,
34
+ payload: overrides.payload || { title: "Backlog" },
35
+ auth: { credentialId: overrides.credentialId || "placeholder", signature: "placeholder" },
36
+ ...overrides
37
+ };
38
+ }
39
+
40
+ function makeKeys(options = {}) {
41
+ const appPackHash = options.appPackHash || APP_HASH;
42
+ const authorityKey = generateOperationGrantAuthorityKeyPair({ issuer: "room-owner" });
43
+ const authority = createOperationGrantAuthority({
44
+ issuer: authorityKey.issuer,
45
+ roomId: ROOM_ID,
46
+ appPackId: kanbanAppPack.id,
47
+ appPackHash,
48
+ publicKeyPem: authorityKey.publicKeyPem,
49
+ issuedAt: 50
50
+ });
51
+ const adminKey = generateOperationRoleKeyPair({ role: "admin" });
52
+ const userKey = generateOperationRoleKeyPair({ role: "user" });
53
+ const unsignedAdminGrant = createOperationRoleKeyGrant({
54
+ roomId: ROOM_ID,
55
+ appPackId: kanbanAppPack.id,
56
+ appPackHash,
57
+ credentialId: "admin_cred",
58
+ memberId: "alice",
59
+ deviceId: "dev_alice",
60
+ role: "admin",
61
+ publicKeyPem: adminKey.publicKeyPem,
62
+ issuedAt: 100
63
+ });
64
+ const unsignedUserGrant = createOperationRoleKeyGrant({
65
+ roomId: ROOM_ID,
66
+ appPackId: kanbanAppPack.id,
67
+ appPackHash,
68
+ credentialId: "user_cred",
69
+ memberId: "bob",
70
+ deviceId: "dev_bob",
71
+ role: "user",
72
+ publicKeyPem: userKey.publicKeyPem,
73
+ issuedAt: 100
74
+ });
75
+ const adminGrant = options.unsigned ? unsignedAdminGrant : signOperationRoleKeyGrant(unsignedAdminGrant, { authority, privateKeyPem: authorityKey.privateKeyPem, signedAt: 150 });
76
+ const userGrant = options.unsigned ? unsignedUserGrant : signOperationRoleKeyGrant(unsignedUserGrant, { authority, privateKeyPem: authorityKey.privateKeyPem, signedAt: 150 });
77
+ return { authorityKey, authority, adminKey, userKey, adminGrant, userGrant, unsignedAdminGrant, unsignedUserGrant, grants: [adminGrant, userGrant], unsignedGrants: [unsignedAdminGrant, unsignedUserGrant], grantAuthorities: [authority] };
78
+ }
79
+
80
+ function sign(op, key, grant) {
81
+ return signOperationWithRoleKey({ ...op, auth: { credentialId: grant.credentialId } }, { privateKeyPem: key.privateKeyPem, grant, now: () => op.createdAt || 1000 });
82
+ }
83
+
84
+ async function enableRelay(keys) {
85
+ const manager = createRelayPluginRuntimeManager({ relayAddress: "relay-role-key", now: () => 5000 });
86
+ await manager.enableRoomComposition({
87
+ roomName: ROOM_ID,
88
+ appPack: kanbanAppPack,
89
+ plugins: kanbanHostPlugins,
90
+ operationRoleKeyGrants: keys.grants,
91
+ operationGrantAuthorities: keys.grantAuthorities
92
+ });
93
+ return manager;
94
+ }
95
+
96
+ test("relay runtime verifies role-key operation signatures without private/master keys", async () => {
97
+ const keys = makeKeys();
98
+ const manager = await enableRelay(keys);
99
+ const app = manager.roomApp(ROOM_ID);
100
+ assert.equal(JSON.stringify(app).includes("PRIVATE KEY"), false);
101
+ assert.equal(app.operationAuth.grants.length, 2);
102
+ assert.equal(app.operationAuth.authorities.length, 1);
103
+ assert.equal(JSON.stringify(app.operationAuth).includes("PRIVATE KEY"), false);
104
+
105
+ const forged = sign(operation({ id: "op_forged", actor: actor("admin", "bob") }), keys.userKey, keys.userGrant);
106
+ const rejected = await manager.handleClientOperation({
107
+ roomName: ROOM_ID,
108
+ peerId: "bob-peer",
109
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "bob", operation: forged },
110
+ sendToClient: () => {},
111
+ broadcastToRoom: () => {}
112
+ });
113
+ assert.equal(rejected.ok, false);
114
+ assert.match(rejected.reason, /Admins only|Forbidden|Operation is not allowed|requires admin/);
115
+
116
+ const adminList = sign(operation({ id: "op_admin", actor: actor("admin", "alice") }), keys.adminKey, keys.adminGrant);
117
+ assert.equal((await manager.handleClientOperation({
118
+ roomName: ROOM_ID,
119
+ peerId: "alice-peer",
120
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "alice", operation: adminList },
121
+ sendToClient: () => {},
122
+ broadcastToRoom: () => {}
123
+ })).ok, true);
124
+
125
+ const listId = (await manager.snapshot(ROOM_ID)).state.plugins[KANBAN_PLUGIN_ID].lists[0].id;
126
+ const userCard = sign(operation({
127
+ id: "op_user_card",
128
+ seq: 2,
129
+ type: "card.create",
130
+ actor: actor("admin", "bob"),
131
+ payload: { listId, title: "Relay accepted user-key member write" },
132
+ createdAt: 1001
133
+ }), keys.userKey, keys.userGrant);
134
+ assert.equal((await manager.handleClientOperation({
135
+ roomName: ROOM_ID,
136
+ peerId: "bob-peer",
137
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "bob", operation: userCard },
138
+ sendToClient: () => {},
139
+ broadcastToRoom: () => {}
140
+ })).ok, true);
141
+ });
142
+
143
+ test("relay-to-relay portable operations reject tampered role-key signatures", async () => {
144
+ const keys = makeKeys();
145
+ const relayA = await enableRelay(keys);
146
+ const relayB = await enableRelay(keys);
147
+ const envelopes = [];
148
+ const adminList = sign(operation({ id: "op_admin", actor: actor("admin", "alice") }), keys.adminKey, keys.adminGrant);
149
+ await relayA.handleClientOperation({
150
+ roomName: ROOM_ID,
151
+ peerId: "alice-peer",
152
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "alice", operation: adminList },
153
+ sendToClient: () => {},
154
+ broadcastToRoom: () => {},
155
+ broadcastToMesh: (message) => envelopes.push(message)
156
+ });
157
+
158
+ const tampered = {
159
+ ...envelopes[0],
160
+ operation: {
161
+ ...envelopes[0].operation,
162
+ payload: { title: "Tampered by relay" }
163
+ }
164
+ };
165
+ const rejected = await relayB.handleRelayOperation(tampered);
166
+ assert.equal(rejected.ok, false);
167
+ assert.match(rejected.reason, /signature|invalid|Forbidden|Admins only|Operation is not allowed|operation\.id|content hash/);
168
+ assert.equal((await relayB.snapshot(ROOM_ID)).state.version, 0);
169
+
170
+ const accepted = await relayB.handleRelayOperation(envelopes[0]);
171
+ assert.equal(accepted.ok, true);
172
+ assert.equal((await relayB.snapshot(ROOM_ID)).state.version, 1);
173
+ });
174
+
175
+ test("relay-owned runtime rejects unsigned operation role-key grants unless explicitly allowed", async () => {
176
+ const keys = makeKeys({ unsigned: true });
177
+ const manager = createRelayPluginRuntimeManager();
178
+ await assert.rejects(() => manager.enableRoomComposition({
179
+ roomName: ROOM_ID,
180
+ appPack: kanbanAppPack,
181
+ plugins: kanbanHostPlugins,
182
+ operationRoleKeyGrants: keys.unsignedGrants
183
+ }), /grant authority|signed|unsigned/i);
184
+
185
+ const devManager = createRelayPluginRuntimeManager({ allowUnsignedOperationRoleKeyGrants: true });
186
+ await devManager.enableRoomComposition({
187
+ roomName: ROOM_ID,
188
+ appPack: kanbanAppPack,
189
+ plugins: kanbanHostPlugins,
190
+ operationRoleKeyGrants: keys.unsignedGrants,
191
+ operationGrantAuthorities: keys.grantAuthorities
192
+ });
193
+ assert.equal(devManager.hasRoom(ROOM_ID), true);
194
+ });
195
+
196
+ test("relay-owned runtime accepts public role-key grants from common collection shapes", async () => {
197
+ const keys = makeKeys();
198
+ const cases = [
199
+ { name: "map", grants: new Map([["admin", keys.adminGrant], ["user", keys.userGrant]]), authorities: keys.grantAuthorities, expected: 2 },
200
+ { name: "single", grants: keys.adminGrant, authorities: keys.grantAuthorities, expected: 1 },
201
+ { name: "object", grants: { admin: keys.adminGrant, user: keys.userGrant }, authorities: { owner: keys.authority }, expected: 2 }
202
+ ];
203
+
204
+ for (const collection of cases) {
205
+ const manager = createRelayPluginRuntimeManager({ relayAddress: `relay-${collection.name}` });
206
+ await manager.enableRoomComposition({
207
+ roomName: ROOM_ID,
208
+ appPack: kanbanAppPack,
209
+ plugins: kanbanHostPlugins,
210
+ operationRoleKeyGrants: collection.grants,
211
+ operationGrantAuthorities: collection.authorities
212
+ });
213
+ assert.equal(manager.roomApp(ROOM_ID).operationAuth.grants.length, collection.expected);
214
+ }
215
+ });
216
+
217
+ test("relay-owned runtime accepts same-app historical signed operations during catch-up", async () => {
218
+ const oldHash = "sha256-old-kanban-schema";
219
+ const keys = makeKeys({ appPackHash: oldHash });
220
+ const manager = createRelayPluginRuntimeManager({ relayAddress: "relay-role-key-upgrade", now: () => 5000 });
221
+ await manager.enableRoomComposition({
222
+ roomName: ROOM_ID,
223
+ appPack: kanbanAppPack,
224
+ plugins: kanbanHostPlugins,
225
+ operationRoleKeyGrants: keys.grants,
226
+ operationGrantAuthorities: keys.grantAuthorities
227
+ });
228
+ const oldOperation = sign(operation({
229
+ id: "op_historical_signed",
230
+ appPackHash: oldHash,
231
+ actor: actor("admin", "alice")
232
+ }), keys.adminKey, keys.adminGrant);
233
+
234
+ const result = await manager.handleRelayOperation({
235
+ type: "relay.matterhorn-operation",
236
+ roomName: ROOM_ID,
237
+ operation: oldOperation
238
+ });
239
+
240
+ assert.equal(result.ok, true, result.reason);
241
+ assert.equal((await manager.snapshot(ROOM_ID)).state.plugins[KANBAN_PLUGIN_ID].lists[0].title, "Backlog");
242
+ });
243
+
244
+ test("relay-owned runtime returns current state with ordinary relay operation rejections", async () => {
245
+ const keys = makeKeys();
246
+ const manager = await enableRelay(keys);
247
+ const forbidden = sign(operation({
248
+ id: "op_forbidden_relay",
249
+ actor: actor("member", "bob")
250
+ }), keys.userKey, keys.userGrant);
251
+
252
+ const result = await manager.handleRelayOperation({
253
+ type: "relay.matterhorn-operation",
254
+ id: "relay-b:forbidden",
255
+ roomName: ROOM_ID,
256
+ operation: forbidden
257
+ });
258
+
259
+ assert.equal(result.ok, false);
260
+ assert.equal(result.state.version, 0);
261
+ });
262
+
263
+ test("secure relay snapshot sync requires replayable signed operation proof", async () => {
264
+ const keys = makeKeys();
265
+ const relayA = await enableRelay(keys);
266
+ const relayB = await enableRelay(keys);
267
+ const envelopes = [];
268
+ const adminList = sign(operation({ id: "op_snapshot_proof", actor: actor("admin", "alice") }), keys.adminKey, keys.adminGrant);
269
+ await relayB.handleClientOperation({
270
+ roomName: ROOM_ID,
271
+ peerId: "alice-peer",
272
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "alice", operation: adminList },
273
+ sendToClient: () => {},
274
+ broadcastToRoom: () => {},
275
+ broadcastToMesh: (message) => envelopes.push(message)
276
+ });
277
+
278
+ const provenSnapshot = await relayB.snapshot(ROOM_ID);
279
+ const tamperedState = {
280
+ ...provenSnapshot.state,
281
+ version: provenSnapshot.state.version + 100,
282
+ plugins: {
283
+ ...provenSnapshot.state.plugins,
284
+ [KANBAN_PLUGIN_ID]: {
285
+ ...provenSnapshot.state.plugins[KANBAN_PLUGIN_ID],
286
+ lists: [{ id: "evil", title: "Forged state", cardIds: [] }]
287
+ }
288
+ }
289
+ };
290
+
291
+ const rejectedBare = await relayA.syncSnapshot(ROOM_ID, tamperedState);
292
+ assert.equal(rejectedBare.updated, false);
293
+ assert.match(rejectedBare.reason, /proof|operation|verify/i);
294
+ assert.equal((await relayA.snapshot(ROOM_ID)).state.version, 0);
295
+
296
+ const accepted = await relayA.syncSnapshot(ROOM_ID, provenSnapshot);
297
+ assert.equal(accepted.updated, true);
298
+ assert.equal((await relayA.snapshot(ROOM_ID)).state.version, provenSnapshot.state.version);
299
+ });
300
+
301
+ test("secure relay operation envelopes can prove missed history for catch-up", async () => {
302
+ const keys = makeKeys();
303
+ const relayA = await enableRelay(keys);
304
+ const relayB = await enableRelay(keys);
305
+
306
+ const listOp = sign(operation({ id: "op_catchup_list", actor: actor("admin", "alice") }), keys.adminKey, keys.adminGrant);
307
+ assert.equal((await relayB.handleClientOperation({
308
+ roomName: ROOM_ID,
309
+ peerId: "alice-peer",
310
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "alice", operation: listOp },
311
+ sendToClient: () => {},
312
+ broadcastToRoom: () => {},
313
+ broadcastToMesh: () => {}
314
+ })).ok, true);
315
+ const listId = (await relayB.snapshot(ROOM_ID)).state.plugins[KANBAN_PLUGIN_ID].lists[0].id;
316
+ const cardOp = sign(operation({
317
+ id: "op_catchup_card",
318
+ seq: 2,
319
+ type: "card.create",
320
+ actor: actor("member", "bob"),
321
+ payload: { listId, title: "Catch up through proof" },
322
+ createdAt: 1001
323
+ }), keys.userKey, keys.userGrant);
324
+ assert.equal((await relayB.handleClientOperation({
325
+ roomName: ROOM_ID,
326
+ peerId: "bob-peer",
327
+ message: { type: "client/operation", protocol: 1, roomName: ROOM_ID, clientId: "bob", operation: cardOp },
328
+ sendToClient: () => {},
329
+ broadcastToRoom: () => {},
330
+ broadcastToMesh: () => {}
331
+ })).ok, true);
332
+
333
+ const provenSnapshot = await relayB.snapshot(ROOM_ID);
334
+ const catchup = await relayA.handleRelayOperation({
335
+ type: "relay.matterhorn-operation",
336
+ id: "relay-b:catchup",
337
+ roomName: ROOM_ID,
338
+ operation: cardOp,
339
+ state: provenSnapshot.state,
340
+ operations: provenSnapshot.operations
341
+ });
342
+ assert.equal(catchup.ok, true);
343
+ const stateA = (await relayA.snapshot(ROOM_ID)).state;
344
+ assert.equal(stateA.version, 2);
345
+ assert.equal(stateA.plugins[KANBAN_PLUGIN_ID].lists[0].cardIds.length, 1);
346
+ });