@mh-gg/host-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 +23 -0
- package/src/constants.cjs +9 -0
- package/src/errors.cjs +39 -0
- package/src/host/installAppPack.cjs +35 -0
- package/src/host/migrations.cjs +21 -0
- package/src/host/runtimeFromPack.cjs +25 -0
- package/src/host/startRoomHost.cjs +95 -0
- package/src/index.cjs +23 -0
- package/src/memory.cjs +81 -0
- package/src/plugins/definition.cjs +19 -0
- package/src/plugins/install.cjs +90 -0
- package/src/plugins/migrations.cjs +27 -0
- package/src/plugins/operationDescriptors.cjs +138 -0
- package/src/runtime/HostPluginRuntime.cjs +85 -0
- package/src/runtime/authorityReplay/applyAuthority.cjs +146 -0
- package/src/runtime/authorityReplay/applyContent.cjs +49 -0
- package/src/runtime/authorityReplay/index.cjs +70 -0
- package/src/runtime/authorityReplay/state.cjs +56 -0
- package/src/runtime/context.cjs +65 -0
- package/src/runtime/coreOperations.cjs +169 -0
- package/src/runtime/corePayloads.cjs +66 -0
- package/src/runtime/directMessages/commit.cjs +50 -0
- package/src/runtime/directMessages/constants.cjs +20 -0
- package/src/runtime/directMessages/helpers.cjs +59 -0
- package/src/runtime/directMessages/payloads.cjs +74 -0
- package/src/runtime/directMessages/state.cjs +168 -0
- package/src/runtime/directMessages.cjs +6 -0
- package/src/runtime/lifecycle.cjs +166 -0
- package/src/runtime/memberProfiles.cjs +74 -0
- package/src/runtime/methods.cjs +10 -0
- package/src/runtime/operations.cjs +166 -0
- package/src/runtime/queries.cjs +146 -0
- package/src/runtime/readTags.cjs +171 -0
- package/src/runtime/scopedRoleOperations.cjs +97 -0
- package/src/runtime/snowflake.cjs +43 -0
- package/src/security/authority/constants.cjs +10 -0
- package/src/security/authority/resolve/operations.cjs +7 -0
- package/src/security/authority/resolve/policy.cjs +7 -0
- package/src/security/authority/resolve/voids.cjs +8 -0
- package/src/security/authorization/coreGate.cjs +63 -0
- package/src/security/authorization/schemaActions.cjs +75 -0
- package/src/security/roleKeys/authenticator.cjs +36 -0
- package/src/security/roleKeys/authorities/index.cjs +4 -0
- package/src/security/roleKeys/authorities/shapes.cjs +98 -0
- package/src/security/roleKeys/authorities/signing.cjs +121 -0
- package/src/security/roleKeys/constants.cjs +15 -0
- package/src/security/roleKeys/fingerprints.cjs +24 -0
- package/src/security/roleKeys/grants.cjs +93 -0
- package/src/security/roleKeys/index.cjs +10 -0
- package/src/security/roleKeys/roles.cjs +21 -0
- package/src/security/roleKeys/signatures.cjs +126 -0
- package/src/security/roles.cjs +10 -0
- package/src/security/roomDeviceKeys.cjs +41 -0
- package/src/security/scopedRoles/access.cjs +123 -0
- package/src/security/scopedRoles/constants.cjs +23 -0
- package/src/security/scopedRoles/metadata.cjs +39 -0
- package/src/security/scopedRoles/normalize.cjs +179 -0
- package/src/security/scopedRoles/publicView.cjs +31 -0
- package/src/security/scopedRoles/stateOps.cjs +167 -0
- package/src/security/scopedRoles.cjs +7 -0
- package/src/security/standingAuthority.cjs +76 -0
- package/src/shared.cjs +14 -0
- package/src/state.cjs +54 -0
- package/test/authority-ordering-hardening.test.cjs +101 -0
- package/test/authorization-gate.test.cjs +610 -0
- package/test/cascading-authority.test.cjs +390 -0
- package/test/grant-authority-security.test.cjs +305 -0
- package/test/matterhorn-host-runtime.test.cjs +1629 -0
- package/test/operation-descriptor-policy.test.cjs +140 -0
- package/test/role-key-auth.test.cjs +289 -0
- package/test/security-isolation.test.cjs +112 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
HostPluginRuntime,
|
|
6
|
+
allow,
|
|
7
|
+
createMemoryOperationLog,
|
|
8
|
+
createMemoryRoomStore,
|
|
9
|
+
createOperationGrantAuthority,
|
|
10
|
+
createOperationRoleKeyGrant,
|
|
11
|
+
createOperationSchemaDescriptor,
|
|
12
|
+
createRoleKeyAuthenticator,
|
|
13
|
+
createSignedOperationRoleKeyGrant,
|
|
14
|
+
defineHostPlugin,
|
|
15
|
+
generateOperationGrantAuthorityKeyPair,
|
|
16
|
+
generateOperationRoleKeyPair,
|
|
17
|
+
signOperationWithRoleKey
|
|
18
|
+
} = require("../src/index.cjs");
|
|
19
|
+
const { ensureOperationIdentity } = require("@mh-gg/protocol");
|
|
20
|
+
|
|
21
|
+
const ROOM_ID = "room_cascade";
|
|
22
|
+
const APP_PACK = { id: "com.matterhorn.cascade", version: "1.0.0", hash: "sha256-cascade", protocolHash: "sha256-protocol" };
|
|
23
|
+
const PLUGIN_ID = "com.matterhorn.cascade.content";
|
|
24
|
+
|
|
25
|
+
function schema(parse) { return { parse }; }
|
|
26
|
+
|
|
27
|
+
function contentPlugin() {
|
|
28
|
+
return defineHostPlugin({
|
|
29
|
+
id: PLUGIN_ID,
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
operationSchemaDescriptor: createOperationSchemaDescriptor({ plugin: PLUGIN_ID, version: "1.0.0", operations: {
|
|
32
|
+
"admin.post": { authorize: { roles: ["admin"] } },
|
|
33
|
+
"member.note": { authorize: { roles: ["member"] } },
|
|
34
|
+
"narrow.post": { authorize: { roles: ["admin"] } }
|
|
35
|
+
} }),
|
|
36
|
+
schemas: {
|
|
37
|
+
state: schema((value) => value && typeof value === "object" ? value : { posts: [] }),
|
|
38
|
+
operations: {
|
|
39
|
+
"admin.post": schema((payload) => {
|
|
40
|
+
assert.equal(typeof payload.id, "string");
|
|
41
|
+
return { id: payload.id, body: payload.body || "" };
|
|
42
|
+
}),
|
|
43
|
+
"member.note": schema((payload) => ({ id: payload.id, body: payload.body || "" })),
|
|
44
|
+
"narrow.post": schema((payload) => ({ id: payload.id, body: payload.body || "" }))
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
createInitialState() { return { posts: [] }; },
|
|
48
|
+
authorize(_ctx, op) {
|
|
49
|
+
if (op.type === "narrow.post") return { ok: false, reason: "narrow denied" };
|
|
50
|
+
return allow();
|
|
51
|
+
},
|
|
52
|
+
reduce(ctx, state, op) {
|
|
53
|
+
return { posts: [...state.posts, { id: op.payload.id, type: op.type, by: ctx.actor.memberId, role: ctx.actor.role }] };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeKeys() {
|
|
59
|
+
const authorityKey = generateOperationGrantAuthorityKeyPair({ issuer: "room-root" });
|
|
60
|
+
const authority = createOperationGrantAuthority({
|
|
61
|
+
issuer: authorityKey.issuer,
|
|
62
|
+
roomId: ROOM_ID,
|
|
63
|
+
appPackId: APP_PACK.id,
|
|
64
|
+
appPackHash: APP_PACK.hash,
|
|
65
|
+
publicKeyPem: authorityKey.publicKeyPem,
|
|
66
|
+
issuedAt: 1
|
|
67
|
+
});
|
|
68
|
+
function member(memberId, role, credentialId = `${memberId}_cred`) {
|
|
69
|
+
const key = generateOperationRoleKeyPair({ role });
|
|
70
|
+
const grant = createSignedOperationRoleKeyGrant(createOperationRoleKeyGrant({
|
|
71
|
+
roomId: ROOM_ID,
|
|
72
|
+
appPackId: APP_PACK.id,
|
|
73
|
+
appPackHash: APP_PACK.hash,
|
|
74
|
+
credentialId,
|
|
75
|
+
memberId,
|
|
76
|
+
deviceId: `dev_${memberId}`,
|
|
77
|
+
role,
|
|
78
|
+
publicKeyPem: key.publicKeyPem,
|
|
79
|
+
issuedAt: 2,
|
|
80
|
+
expiresAt: 100000
|
|
81
|
+
}), { authority, privateKeyPem: authorityKey.privateKeyPem, signedAt: 3 });
|
|
82
|
+
return { key, grant };
|
|
83
|
+
}
|
|
84
|
+
const olivia = member("olivia", "owner");
|
|
85
|
+
const alice = member("alice", "admin");
|
|
86
|
+
const bob = member("bob", "admin");
|
|
87
|
+
const carol = member("carol", "admin");
|
|
88
|
+
const realAdmin = member("realAdmin", "admin");
|
|
89
|
+
const memberOnly = member("memberOnly", "member");
|
|
90
|
+
return { authority, olivia, alice, bob, carol, realAdmin, memberOnly, grants: [olivia.grant, alice.grant, bob.grant, carol.grant, realAdmin.grant, memberOnly.grant] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function initialAuthorityGrants() {
|
|
94
|
+
return [
|
|
95
|
+
{ id: "grant_root_alice_admin", granter: "olivia", target: "alice", role: "admin", createdAt: 10 },
|
|
96
|
+
{ id: "grant_root_real_admin", granter: "olivia", target: "realAdmin", role: "admin", createdAt: 11 },
|
|
97
|
+
{ id: "grant_root_bob_member", granter: "olivia", target: "bob", role: "member", createdAt: 12 },
|
|
98
|
+
{ id: "grant_root_carol_member", granter: "olivia", target: "carol", role: "member", createdAt: 13 },
|
|
99
|
+
{ id: "grant_root_memberOnly_member", granter: "olivia", target: "memberOnly", role: "member", createdAt: 14 }
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createRuntime() {
|
|
104
|
+
const keys = makeKeys();
|
|
105
|
+
const operationLog = createMemoryOperationLog();
|
|
106
|
+
const runtime = new HostPluginRuntime({
|
|
107
|
+
room: { id: ROOM_ID, appPack: APP_PACK },
|
|
108
|
+
plugins: [contentPlugin()],
|
|
109
|
+
store: createMemoryRoomStore(),
|
|
110
|
+
operationLog,
|
|
111
|
+
initialOwners: ["olivia"],
|
|
112
|
+
initialAuthorityGrants: initialAuthorityGrants(),
|
|
113
|
+
authenticateActor: createRoleKeyAuthenticator({
|
|
114
|
+
roomId: ROOM_ID,
|
|
115
|
+
appPackId: APP_PACK.id,
|
|
116
|
+
appPackHash: APP_PACK.hash,
|
|
117
|
+
grants: keys.grants,
|
|
118
|
+
grantAuthorities: [keys.authority],
|
|
119
|
+
requireSignedGrants: true,
|
|
120
|
+
now: () => 100
|
|
121
|
+
}),
|
|
122
|
+
now: () => 1000
|
|
123
|
+
});
|
|
124
|
+
return { runtime, operationLog, keys };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function unsigned(memberId, pluginId, type, payload, overrides = {}) {
|
|
128
|
+
return {
|
|
129
|
+
id: overrides.id || `op_${memberId}_${type.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
130
|
+
roomId: ROOM_ID,
|
|
131
|
+
appPackId: APP_PACK.id,
|
|
132
|
+
appPackHash: APP_PACK.hash,
|
|
133
|
+
pluginId,
|
|
134
|
+
type,
|
|
135
|
+
actor: { memberId, deviceId: `dev_${memberId}`, role: overrides.claimedRole || "member" },
|
|
136
|
+
seq: overrides.seq || 1,
|
|
137
|
+
createdAt: overrides.createdAt || 1000,
|
|
138
|
+
payload: payload || {},
|
|
139
|
+
auth: { credentialId: overrides.credentialId || `${memberId}_cred`, signature: "placeholder" }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sign(op, memberKeys) {
|
|
144
|
+
return signOperationWithRoleKey(op, { privateKeyPem: memberKeys.key.privateKeyPem, grant: memberKeys.grant, now: () => op.createdAt });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function grantOp(memberId, target, role, memberKeys, overrides = {}) {
|
|
148
|
+
return sign(unsigned(memberId, "matterhorn.core", "authority.grant", { target, role, reason: overrides.reason || "test" }, {
|
|
149
|
+
id: overrides.id,
|
|
150
|
+
seq: overrides.seq,
|
|
151
|
+
createdAt: overrides.createdAt,
|
|
152
|
+
claimedRole: overrides.claimedRole || role
|
|
153
|
+
}), memberKeys);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function revokeOp(memberId, payload, memberKeys, overrides = {}) {
|
|
157
|
+
return sign(unsigned(memberId, "matterhorn.core", "authority.revoke", payload, {
|
|
158
|
+
id: overrides.id,
|
|
159
|
+
seq: overrides.seq,
|
|
160
|
+
createdAt: overrides.createdAt,
|
|
161
|
+
claimedRole: overrides.claimedRole || "owner"
|
|
162
|
+
}), memberKeys);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function postOp(memberId, id, memberKeys, overrides = {}) {
|
|
166
|
+
return sign(unsigned(memberId, PLUGIN_ID, "admin.post", { id, body: id }, {
|
|
167
|
+
id: overrides.id || `op_${id}`,
|
|
168
|
+
seq: overrides.seq,
|
|
169
|
+
createdAt: overrides.createdAt,
|
|
170
|
+
claimedRole: overrides.claimedRole || "admin"
|
|
171
|
+
}), memberKeys);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function expectOk(runtime, op) {
|
|
175
|
+
const result = await runtime.handleOperation(op);
|
|
176
|
+
assert.equal(result.ok, true, result.reason || result.code);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function expectDenied(runtime, op, pattern) {
|
|
181
|
+
const result = await runtime.handleOperation(op);
|
|
182
|
+
assert.equal(result.ok, false);
|
|
183
|
+
assert.match(result.reason || result.code || "", pattern);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test("owner-root revocation cascades through malicious admin grants and downstream admin writes", async () => {
|
|
188
|
+
const { runtime, keys } = createRuntime();
|
|
189
|
+
await runtime.start();
|
|
190
|
+
|
|
191
|
+
const grantBobAdmin = grantOp("alice", "bob", "admin", keys.alice, { id: "op_alice_grants_bob_admin", seq: 1, createdAt: 1001 });
|
|
192
|
+
await expectOk(runtime, grantBobAdmin);
|
|
193
|
+
const grantedState = await runtime.getState();
|
|
194
|
+
const grantRecord = grantedState.authority.grants.find((grant) => grant.id === grantBobAdmin.id);
|
|
195
|
+
assert.equal(grantRecord.credentialId, "alice_cred");
|
|
196
|
+
assert.equal(typeof grantRecord.signature, "string");
|
|
197
|
+
assert.equal(grantRecord.createdAt, 1001);
|
|
198
|
+
const bobAdminPost = postOp("bob", "bob_admin_post", keys.bob, { id: "op_bob_admin_post", seq: 2, createdAt: 1002 });
|
|
199
|
+
await expectOk(runtime, bobAdminPost);
|
|
200
|
+
|
|
201
|
+
const grantCarolAdmin = grantOp("bob", "carol", "admin", keys.bob, { id: "op_bob_grants_carol_admin", seq: 3, createdAt: 1003 });
|
|
202
|
+
await expectOk(runtime, grantCarolAdmin);
|
|
203
|
+
const carolAdminPost = postOp("carol", "carol_admin_post", keys.carol, { id: "op_carol_admin_post", seq: 4, createdAt: 1004 });
|
|
204
|
+
await expectOk(runtime, carolAdminPost);
|
|
205
|
+
|
|
206
|
+
const demoteRealAdmin = grantOp("alice", "realAdmin", "member", keys.alice, { id: "op_alice_demotes_real_admin", seq: 5, createdAt: 1005 });
|
|
207
|
+
await expectOk(runtime, demoteRealAdmin);
|
|
208
|
+
|
|
209
|
+
await expectOk(runtime, revokeOp("olivia", {
|
|
210
|
+
target: "alice",
|
|
211
|
+
voidGrantIds: [grantBobAdmin.id, demoteRealAdmin.id],
|
|
212
|
+
scope: "all-by-target",
|
|
213
|
+
cascade: true
|
|
214
|
+
}, keys.olivia, { id: "op_olivia_revokes_alice_spree", seq: 6, createdAt: 1006 }));
|
|
215
|
+
|
|
216
|
+
const state = await runtime.getState();
|
|
217
|
+
assert.equal(state.members.olivia.role, "owner");
|
|
218
|
+
assert.equal(state.members.alice.role, "admin");
|
|
219
|
+
assert.equal(state.members.realAdmin.role, "admin");
|
|
220
|
+
assert.equal(state.members.bob.role, "member");
|
|
221
|
+
assert.equal(state.members.carol.role, "member");
|
|
222
|
+
assert.deepEqual(state.plugins[PLUGIN_ID].posts, []);
|
|
223
|
+
assert.equal(state.authority.tombstones[grantBobAdmin.id].reason, "voided-by-revocation");
|
|
224
|
+
assert.equal(state.authority.tombstones[demoteRealAdmin.id].reason, "voided-by-revocation");
|
|
225
|
+
assert.equal(state.authority.tombstones[bobAdminPost.id].reason, "unauthorized-operation");
|
|
226
|
+
assert.equal(state.authority.tombstones[grantCarolAdmin.id].reason, "unauthorized-authority-grant");
|
|
227
|
+
assert.equal(state.authority.tombstones[carolAdminPost.id].reason, "unauthorized-operation");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("admin cannot demote or revoke a genesis owner", async () => {
|
|
231
|
+
const { runtime, keys } = createRuntime();
|
|
232
|
+
await runtime.start();
|
|
233
|
+
|
|
234
|
+
await expectDenied(runtime, grantOp("alice", "olivia", "member", keys.alice, {
|
|
235
|
+
id: "op_alice_demotes_owner",
|
|
236
|
+
createdAt: 2001,
|
|
237
|
+
claimedRole: "admin"
|
|
238
|
+
}), /owner|authority|forbidden/i);
|
|
239
|
+
|
|
240
|
+
const state = await runtime.getState();
|
|
241
|
+
assert.equal(state.members.olivia.role, "owner");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("from-point revocation leaves pre-cut grants and content intact", async () => {
|
|
245
|
+
const { runtime, keys } = createRuntime();
|
|
246
|
+
await runtime.start();
|
|
247
|
+
|
|
248
|
+
await expectOk(runtime, postOp("alice", "legit_pre_cut", keys.alice, { id: "op_alice_pre_cut", createdAt: 2999 }));
|
|
249
|
+
const grantBobAdmin = grantOp("alice", "bob", "admin", keys.alice, { id: "op_alice_late_bob_admin", createdAt: 3001 });
|
|
250
|
+
await expectOk(runtime, grantBobAdmin);
|
|
251
|
+
await expectOk(runtime, postOp("bob", "late_bob_post", keys.bob, { id: "op_late_bob_post", createdAt: 3002 }));
|
|
252
|
+
|
|
253
|
+
await expectOk(runtime, revokeOp("olivia", {
|
|
254
|
+
target: "alice",
|
|
255
|
+
voidFrom: grantBobAdmin.hlc,
|
|
256
|
+
scope: "from-point",
|
|
257
|
+
cascade: true
|
|
258
|
+
}, keys.olivia, { id: "op_olivia_from_point", createdAt: 3003 }));
|
|
259
|
+
|
|
260
|
+
const state = await runtime.getState();
|
|
261
|
+
assert.deepEqual(state.plugins[PLUGIN_ID].posts.map((post) => post.id), ["legit_pre_cut"]);
|
|
262
|
+
assert.equal(state.members.bob.role, "member");
|
|
263
|
+
assert.equal(state.authority.tombstones[grantBobAdmin.id].reason, "voided-by-revocation");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const { createAuthorityState, resolveAuthorityOperations } = require("../src/index.cjs");
|
|
267
|
+
|
|
268
|
+
function coreUnsigned(memberId, type, payload, id, createdAt, overrides = {}) {
|
|
269
|
+
return ensureOperationIdentity({
|
|
270
|
+
roomId: ROOM_ID,
|
|
271
|
+
appPackId: APP_PACK.id,
|
|
272
|
+
appPackHash: APP_PACK.hash,
|
|
273
|
+
pluginId: overrides.pluginId || "matterhorn.core",
|
|
274
|
+
type,
|
|
275
|
+
actor: { memberId, deviceId: `dev_${memberId}`, role: "admin" },
|
|
276
|
+
seq: 1,
|
|
277
|
+
createdAt,
|
|
278
|
+
hlc: overrides.hlc,
|
|
279
|
+
payload,
|
|
280
|
+
auth: { credentialId: `${memberId}_cred`, signature: "sig" }
|
|
281
|
+
}, { now: createdAt, nodeId: `dev_${memberId}` });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
test("authority resolution is deterministic across shuffled arrival order", () => {
|
|
285
|
+
const authority = createAuthorityState({
|
|
286
|
+
ownerIds: ["olivia"],
|
|
287
|
+
grants: [
|
|
288
|
+
{ id: "grant_alice_admin", granter: "olivia", target: "alice", role: "admin", createdAt: 10 },
|
|
289
|
+
{ id: "grant_bob_member", granter: "olivia", target: "bob", role: "member", createdAt: 11 }
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
const grantBobAdmin = coreUnsigned("alice", "authority.grant", { target: "bob", role: "admin" }, "op_grant_bob_admin", 100);
|
|
293
|
+
const revokeAliceGrant = coreUnsigned("olivia", "authority.revoke", { target: "alice", voidGrantIds: [grantBobAdmin.id], scope: "all-by-target", cascade: true }, "op_revoke_alice_grant", 101);
|
|
294
|
+
|
|
295
|
+
const left = resolveAuthorityOperations({ authority, operations: [grantBobAdmin, revokeAliceGrant] });
|
|
296
|
+
const right = resolveAuthorityOperations({ authority, operations: [revokeAliceGrant, grantBobAdmin] });
|
|
297
|
+
|
|
298
|
+
assert.deepEqual(left.derivedRoles, right.derivedRoles);
|
|
299
|
+
assert.deepEqual(left.tombstones, right.tombstones);
|
|
300
|
+
assert.equal(left.derivedRoles.bob, "member");
|
|
301
|
+
assert.equal(left.tombstones[grantBobAdmin.id].reason, "voided-by-revocation");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("concurrent mutual admin revocation converges by deterministic operation order", () => {
|
|
305
|
+
const authority = createAuthorityState({
|
|
306
|
+
ownerIds: ["olivia"],
|
|
307
|
+
grants: [
|
|
308
|
+
{ id: "grant_alice_admin", granter: "olivia", target: "alice", role: "admin", createdAt: 10 },
|
|
309
|
+
{ id: "grant_bob_admin", granter: "olivia", target: "bob", role: "admin", createdAt: 11 }
|
|
310
|
+
]
|
|
311
|
+
});
|
|
312
|
+
const aliceRevokesBob = coreUnsigned("alice", "authority.revoke", { target: "bob", voidGrantIds: ["grant_bob_admin"], scope: "all-by-target", cascade: true }, "a_revokes_bob", 100);
|
|
313
|
+
const bobRevokesAlice = coreUnsigned("bob", "authority.revoke", { target: "alice", voidGrantIds: ["grant_alice_admin"], scope: "all-by-target", cascade: true }, "b_revokes_alice", 100);
|
|
314
|
+
|
|
315
|
+
const resolved = resolveAuthorityOperations({ authority, operations: [bobRevokesAlice, aliceRevokesBob] });
|
|
316
|
+
|
|
317
|
+
assert.equal(resolved.derivedRoles.alice, "admin");
|
|
318
|
+
assert.equal(resolved.derivedRoles.bob, undefined);
|
|
319
|
+
assert.equal(resolved.tombstones.grant_bob_admin.reason, "voided-by-revocation");
|
|
320
|
+
assert.equal(resolved.tombstones[bobRevokesAlice.id].reason, "unauthorized-authority-revocation");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("authority replay tombstones malformed, unknown, and narrowing-denied log entries", async () => {
|
|
324
|
+
const { runtime, operationLog, keys } = createRuntime();
|
|
325
|
+
await runtime.start();
|
|
326
|
+
|
|
327
|
+
const missingPlugin = coreUnsigned("alice", "not.core", {}, "op_missing_plugin", 401, { pluginId: "com.matterhorn.missing" });
|
|
328
|
+
const unknownType = coreUnsigned("alice", "unknown.type", {}, "op_unknown_type", 402, { pluginId: PLUGIN_ID });
|
|
329
|
+
const invalidPayload = sign(unsigned("alice", PLUGIN_ID, "admin.post", {}, { id: "op_invalid_payload", createdAt: 403, claimedRole: "admin" }), keys.alice);
|
|
330
|
+
const pluginDenied = sign(unsigned("alice", PLUGIN_ID, "narrow.post", { id: "narrow" }, { id: "op_plugin_denied", createdAt: 404, claimedRole: "admin" }), keys.alice);
|
|
331
|
+
const invalidCredentialRevoke = sign(unsigned("alice", "matterhorn.core", "credential.revoke", {}, { id: "op_invalid_credential_revoke", createdAt: 405, claimedRole: "admin" }), keys.alice);
|
|
332
|
+
const unauthorizedCredentialRevoke = sign(unsigned("memberOnly", "matterhorn.core", "credential.revoke", { credentialId: "alice_cred" }, { id: "op_unauthorized_credential_revoke", createdAt: 406, claimedRole: "member" }), keys.memberOnly);
|
|
333
|
+
operationLog.entries.push(missingPlugin, unknownType, invalidPayload, pluginDenied, invalidCredentialRevoke, unauthorizedCredentialRevoke);
|
|
334
|
+
|
|
335
|
+
await expectOk(runtime, revokeOp("olivia", {
|
|
336
|
+
target: "alice",
|
|
337
|
+
scope: "future",
|
|
338
|
+
cascade: true
|
|
339
|
+
}, keys.olivia, { id: "op_replay_cleanup", createdAt: 407 }));
|
|
340
|
+
|
|
341
|
+
const state = await runtime.getState();
|
|
342
|
+
assert.equal(state.authority.tombstones[missingPlugin.id].reason, "plugin-not-installed");
|
|
343
|
+
assert.equal(state.authority.tombstones[unknownType.id].reason, "unknown-operation-type");
|
|
344
|
+
assert.equal(state.authority.tombstones[invalidPayload.id].reason, "invalid-payload");
|
|
345
|
+
assert.equal(state.authority.tombstones[pluginDenied.id].reason, "plugin-denied");
|
|
346
|
+
assert.equal(state.authority.tombstones[invalidCredentialRevoke.id].reason, "invalid-credential-revocation");
|
|
347
|
+
assert.equal(state.authority.tombstones[unauthorizedCredentialRevoke.id].reason, "unauthorized-credential-revocation");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("authority ordering honors HLC before createdAt and id fallback", () => {
|
|
351
|
+
const { sortOperations } = require("../src/index.cjs");
|
|
352
|
+
const ordered = sortOperations([
|
|
353
|
+
{ id: "late", createdAt: 1, hlc: "000000000000002:000000:node" },
|
|
354
|
+
{ id: "early-b", createdAt: 99, hlc: "000000000000001:000000:node" },
|
|
355
|
+
{ id: "early-a", createdAt: 1, hlc: "000000000000001:000000:node" }
|
|
356
|
+
]);
|
|
357
|
+
assert.deepEqual(ordered.map((op) => op.id), ["early-a", "early-b", "late"]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("authority resolver and replay record invalid authority entries deterministically", async () => {
|
|
361
|
+
const authority = createAuthorityState({ ownerIds: ["olivia"] });
|
|
362
|
+
const badGrant = coreUnsigned("olivia", "authority.grant", { target: "", role: "admin" }, "op_bad_grant", 1);
|
|
363
|
+
const badRevoke = ensureOperationIdentity({
|
|
364
|
+
roomId: ROOM_ID,
|
|
365
|
+
appPackId: APP_PACK.id,
|
|
366
|
+
appPackHash: APP_PACK.hash,
|
|
367
|
+
pluginId: "matterhorn.core",
|
|
368
|
+
type: "authority.revoke",
|
|
369
|
+
actor: {},
|
|
370
|
+
seq: 1,
|
|
371
|
+
createdAt: 2,
|
|
372
|
+
payload: { scope: "future" },
|
|
373
|
+
auth: { credentialId: "olivia_cred", signature: "sig" }
|
|
374
|
+
}, { now: 2, nodeId: "dev_olivia" });
|
|
375
|
+
const resolved = resolveAuthorityOperations({ authority, operations: [badGrant, badRevoke] });
|
|
376
|
+
assert.equal(resolved.tombstones[badGrant.id].reason, "invalid-authority-grant");
|
|
377
|
+
assert.equal(resolved.tombstones[badRevoke.id].reason, "invalid-authority-revocation");
|
|
378
|
+
|
|
379
|
+
const { runtime, operationLog, keys } = createRuntime();
|
|
380
|
+
await runtime.start();
|
|
381
|
+
const replayBadGrant = coreUnsigned("alice", "authority.grant", { target: "", role: "admin" }, "op_replay_bad_grant", 501);
|
|
382
|
+
const wrongRoom = { ...coreUnsigned("alice", "authority.grant", { target: "bob", role: "admin" }, "op_wrong_room", 502), roomId: "other_room" };
|
|
383
|
+
operationLog.entries.push(replayBadGrant);
|
|
384
|
+
operationLog.entries.push(wrongRoom);
|
|
385
|
+
|
|
386
|
+
await expectOk(runtime, revokeOp("olivia", { target: "alice", scope: "future", cascade: true }, keys.olivia, { id: "op_trigger_invalid_replay", createdAt: 503 }));
|
|
387
|
+
const state = await runtime.getState();
|
|
388
|
+
assert.equal(state.authority.tombstones[replayBadGrant.id].reason, "invalid-authority-grant");
|
|
389
|
+
assert.equal(state.authority.tombstones[wrongRoom.id].reason, "invalid-operation");
|
|
390
|
+
});
|