@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.
Files changed (71) hide show
  1. package/package.json +23 -0
  2. package/src/constants.cjs +9 -0
  3. package/src/errors.cjs +39 -0
  4. package/src/host/installAppPack.cjs +35 -0
  5. package/src/host/migrations.cjs +21 -0
  6. package/src/host/runtimeFromPack.cjs +25 -0
  7. package/src/host/startRoomHost.cjs +95 -0
  8. package/src/index.cjs +23 -0
  9. package/src/memory.cjs +81 -0
  10. package/src/plugins/definition.cjs +19 -0
  11. package/src/plugins/install.cjs +90 -0
  12. package/src/plugins/migrations.cjs +27 -0
  13. package/src/plugins/operationDescriptors.cjs +138 -0
  14. package/src/runtime/HostPluginRuntime.cjs +85 -0
  15. package/src/runtime/authorityReplay/applyAuthority.cjs +146 -0
  16. package/src/runtime/authorityReplay/applyContent.cjs +49 -0
  17. package/src/runtime/authorityReplay/index.cjs +70 -0
  18. package/src/runtime/authorityReplay/state.cjs +56 -0
  19. package/src/runtime/context.cjs +65 -0
  20. package/src/runtime/coreOperations.cjs +169 -0
  21. package/src/runtime/corePayloads.cjs +66 -0
  22. package/src/runtime/directMessages/commit.cjs +50 -0
  23. package/src/runtime/directMessages/constants.cjs +20 -0
  24. package/src/runtime/directMessages/helpers.cjs +59 -0
  25. package/src/runtime/directMessages/payloads.cjs +74 -0
  26. package/src/runtime/directMessages/state.cjs +168 -0
  27. package/src/runtime/directMessages.cjs +6 -0
  28. package/src/runtime/lifecycle.cjs +166 -0
  29. package/src/runtime/memberProfiles.cjs +74 -0
  30. package/src/runtime/methods.cjs +10 -0
  31. package/src/runtime/operations.cjs +166 -0
  32. package/src/runtime/queries.cjs +146 -0
  33. package/src/runtime/readTags.cjs +171 -0
  34. package/src/runtime/scopedRoleOperations.cjs +97 -0
  35. package/src/runtime/snowflake.cjs +43 -0
  36. package/src/security/authority/constants.cjs +10 -0
  37. package/src/security/authority/resolve/operations.cjs +7 -0
  38. package/src/security/authority/resolve/policy.cjs +7 -0
  39. package/src/security/authority/resolve/voids.cjs +8 -0
  40. package/src/security/authorization/coreGate.cjs +63 -0
  41. package/src/security/authorization/schemaActions.cjs +75 -0
  42. package/src/security/roleKeys/authenticator.cjs +36 -0
  43. package/src/security/roleKeys/authorities/index.cjs +4 -0
  44. package/src/security/roleKeys/authorities/shapes.cjs +98 -0
  45. package/src/security/roleKeys/authorities/signing.cjs +121 -0
  46. package/src/security/roleKeys/constants.cjs +15 -0
  47. package/src/security/roleKeys/fingerprints.cjs +24 -0
  48. package/src/security/roleKeys/grants.cjs +93 -0
  49. package/src/security/roleKeys/index.cjs +10 -0
  50. package/src/security/roleKeys/roles.cjs +21 -0
  51. package/src/security/roleKeys/signatures.cjs +126 -0
  52. package/src/security/roles.cjs +10 -0
  53. package/src/security/roomDeviceKeys.cjs +41 -0
  54. package/src/security/scopedRoles/access.cjs +123 -0
  55. package/src/security/scopedRoles/constants.cjs +23 -0
  56. package/src/security/scopedRoles/metadata.cjs +39 -0
  57. package/src/security/scopedRoles/normalize.cjs +179 -0
  58. package/src/security/scopedRoles/publicView.cjs +31 -0
  59. package/src/security/scopedRoles/stateOps.cjs +167 -0
  60. package/src/security/scopedRoles.cjs +7 -0
  61. package/src/security/standingAuthority.cjs +76 -0
  62. package/src/shared.cjs +14 -0
  63. package/src/state.cjs +54 -0
  64. package/test/authority-ordering-hardening.test.cjs +101 -0
  65. package/test/authorization-gate.test.cjs +610 -0
  66. package/test/cascading-authority.test.cjs +390 -0
  67. package/test/grant-authority-security.test.cjs +305 -0
  68. package/test/matterhorn-host-runtime.test.cjs +1629 -0
  69. package/test/operation-descriptor-policy.test.cjs +140 -0
  70. package/test/role-key-auth.test.cjs +289 -0
  71. package/test/security-isolation.test.cjs +112 -0
@@ -0,0 +1,167 @@
1
+ const { runtimeError } = require("../../errors.cjs");
2
+ const {
3
+ boundedString,
4
+ normalizeRoleGrant,
5
+ normalizeScopedRole,
6
+ normalizeScopedRolesState,
7
+ optionalBoundedString,
8
+ scopedRoleFromGlobalRole,
9
+ scopedRoleRank,
10
+ scopeKey
11
+ } = require("./normalize.cjs");
12
+ const { SCOPED_ROLES_STATE_VERSION } = require("./constants.cjs");
13
+ const { grantMetadata, revocationMetadata } = require("./metadata.cjs");
14
+
15
+ function parseScopeRoleSetPayload(operation) {
16
+ const payload = operation.payload || {};
17
+ const target = optionalBoundedString(payload.target, "target", 240);
18
+ const role = normalizeScopedRole(payload.role, undefined);
19
+ const defaultRole = normalizeScopedRole(payload.defaultRole, undefined);
20
+ if (target && !role) throw runtimeError("SCHEMA_INVALID", "scope.role.set role is invalid");
21
+ if (!target && !defaultRole) throw runtimeError("SCHEMA_INVALID", "scope.role.set requires target+role or defaultRole");
22
+ return {
23
+ scopeType: boundedString(payload.scopeType, "scopeType", 80),
24
+ scopeId: boundedString(payload.scopeId, "scopeId", 180),
25
+ ...(target ? { target, role } : {}),
26
+ ...(defaultRole ? { defaultRole } : {}),
27
+ ...(optionalBoundedString(payload.reason, "reason", 500) ? { reason: payload.reason.trim().slice(0, 500) } : {})
28
+ };
29
+ }
30
+
31
+ function parseAccessRoleDefinePayload(operation) {
32
+ const payload = operation.payload || {};
33
+ const roleId = boundedString(payload.roleId || payload.id, "roleId", 120);
34
+ const grantsInput = Array.isArray(payload.grants) ? payload.grants : [];
35
+ if (grantsInput.length === 0) throw runtimeError("SCHEMA_INVALID", "access.role.define requires at least one grant");
36
+ return {
37
+ roleId,
38
+ name: optionalBoundedString(payload.name, "name", 120) || roleId,
39
+ grants: grantsInput.map((grant, index) => {
40
+ const parsed = normalizeRoleGrant(grant, index);
41
+ if (!parsed) throw runtimeError("SCHEMA_INVALID", `grants[${index}] is invalid`);
42
+ return parsed;
43
+ }),
44
+ ...(optionalBoundedString(payload.reason, "reason", 500) ? { reason: payload.reason.trim().slice(0, 500) } : {})
45
+ };
46
+ }
47
+
48
+ function parseAccessRoleAssignmentPayload(operation) {
49
+ const payload = operation.payload || {};
50
+ return {
51
+ target: boundedString(payload.target, "target", 240),
52
+ roleId: boundedString(payload.roleId || payload.id, "roleId", 120),
53
+ ...(optionalBoundedString(payload.reason, "reason", 500) ? { reason: payload.reason.trim().slice(0, 500) } : {})
54
+ };
55
+ }
56
+
57
+ function nextScopedRolesState(currentState, payload, actor = {}, updatedAt = Date.now(), operation) {
58
+ const state = normalizeScopedRolesState(currentState);
59
+ const key = scopeKey(payload.scopeType, payload.scopeId);
60
+ const current = state.scopes[key] || { scopeType: payload.scopeType, scopeId: payload.scopeId, defaultRole: "editor", roles: {} };
61
+ const roles = { ...(current.roles || {}) };
62
+ if (payload.target) roles[payload.target] = payload.role;
63
+ const grant = grantMetadata(operation, actor, payload, updatedAt, "scope.role.set");
64
+ const grants = grant ? { ...state.grants, [grant.id]: grant } : state.grants;
65
+ return {
66
+ version: SCOPED_ROLES_STATE_VERSION,
67
+ roles: state.roles,
68
+ assignments: state.assignments,
69
+ assignmentGrants: state.assignmentGrants,
70
+ grants,
71
+ revocations: state.revocations,
72
+ scopes: {
73
+ ...state.scopes,
74
+ [key]: {
75
+ ...current,
76
+ ...(payload.defaultRole ? { defaultRole: payload.defaultRole } : {}),
77
+ roles,
78
+ updatedAt,
79
+ updatedBy: actor.memberId
80
+ }
81
+ }
82
+ };
83
+ }
84
+
85
+ function nextCompoundRoleState(currentState, payload, actor = {}, updatedAt = Date.now(), operation) {
86
+ const state = normalizeScopedRolesState(currentState);
87
+ const grant = grantMetadata(operation, actor, payload, updatedAt, "access.role.define");
88
+ return {
89
+ ...state,
90
+ grants: grant ? { ...state.grants, [grant.id]: grant } : state.grants,
91
+ roles: {
92
+ ...state.roles,
93
+ [payload.roleId]: {
94
+ id: payload.roleId,
95
+ name: payload.name,
96
+ grants: payload.grants,
97
+ ...(grant ? { authority: grant } : {}),
98
+ updatedAt,
99
+ updatedBy: actor.memberId
100
+ }
101
+ }
102
+ };
103
+ }
104
+
105
+ function nextRoleAssignmentState(currentState, payload, assigned, actor = {}, updatedAt = Date.now(), operation) {
106
+ const state = normalizeScopedRolesState(currentState);
107
+ if (!state.roles[payload.roleId]) throw runtimeError("SCHEMA_INVALID", `Compound role ${payload.roleId} is not defined`);
108
+ const current = new Set(state.assignments[payload.target] || []);
109
+ if (assigned) current.add(payload.roleId);
110
+ else current.delete(payload.roleId);
111
+ const assignments = { ...state.assignments };
112
+ if (current.size) assignments[payload.target] = [...current].sort();
113
+ else delete assignments[payload.target];
114
+ const assignmentGrants = {
115
+ ...state.assignmentGrants,
116
+ [payload.target]: { ...(state.assignmentGrants[payload.target] || {}) }
117
+ };
118
+ const grants = { ...state.grants };
119
+ const revocations = { ...state.revocations };
120
+ if (assigned) {
121
+ const grant = grantMetadata(operation, actor, payload, updatedAt, "access.role.assign");
122
+ if (grant) {
123
+ grants[grant.id] = grant;
124
+ assignmentGrants[payload.target][payload.roleId] = grant.id;
125
+ }
126
+ } else {
127
+ const priorGrantId = assignmentGrants[payload.target][payload.roleId];
128
+ delete assignmentGrants[payload.target][payload.roleId];
129
+ if (Object.keys(assignmentGrants[payload.target]).length === 0) delete assignmentGrants[payload.target];
130
+ const revocation = revocationMetadata(operation, actor, payload, updatedAt, priorGrantId ? [priorGrantId] : []);
131
+ if (revocation) revocations[revocation.id] = revocation;
132
+ }
133
+ return { ...state, assignments, assignmentGrants, grants, revocations, updatedAt, updatedBy: actor.memberId };
134
+ }
135
+
136
+ function applyScopeRoleSetToRoomState(roomState, payload, actor, updatedAt, operation) {
137
+ return { ...roomState, scopedRoles: nextScopedRolesState(roomState?.scopedRoles, payload, actor, updatedAt, operation) };
138
+ }
139
+
140
+ function applyAccessRoleDefineToRoomState(roomState, payload, actor, updatedAt, operation) {
141
+ return { ...roomState, scopedRoles: nextCompoundRoleState(roomState?.scopedRoles, payload, actor, updatedAt, operation) };
142
+ }
143
+
144
+ function applyAccessRoleAssignmentToRoomState(roomState, payload, assigned, actor, updatedAt, operation) {
145
+ return { ...roomState, scopedRoles: nextRoleAssignmentState(roomState?.scopedRoles, payload, assigned, actor, updatedAt, operation) };
146
+ }
147
+
148
+ function assertScopedRoleManagerCanApply(actor, payload) {
149
+ if (scopedRoleRank(scopedRoleFromGlobalRole(actor?.role)) >= scopedRoleRank("admin")) return;
150
+ if (scopedRoleRank(scopedRoleFromGlobalRole(actor?.role)) < scopedRoleRank("moderator")) throw runtimeError("FORBIDDEN", "Scoped roles require moderator authority");
151
+ if ((payload.grants || []).some((grant) => scopedRoleRank(grant.role) > scopedRoleRank("moderator"))) {
152
+ throw runtimeError("FORBIDDEN", "Moderators cannot define scoped roles above moderator");
153
+ }
154
+ }
155
+
156
+ module.exports = {
157
+ applyAccessRoleAssignmentToRoomState,
158
+ applyAccessRoleDefineToRoomState,
159
+ applyScopeRoleSetToRoomState,
160
+ assertScopedRoleManagerCanApply,
161
+ nextCompoundRoleState,
162
+ nextRoleAssignmentState,
163
+ nextScopedRolesState,
164
+ parseAccessRoleAssignmentPayload,
165
+ parseAccessRoleDefinePayload,
166
+ parseScopeRoleSetPayload
167
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ ...require("./scopedRoles/constants.cjs"),
3
+ ...require("./scopedRoles/normalize.cjs"),
4
+ ...require("./scopedRoles/access.cjs"),
5
+ ...require("./scopedRoles/stateOps.cjs"),
6
+ ...require("./scopedRoles/publicView.cjs")
7
+ };
@@ -0,0 +1,76 @@
1
+ const { effectiveRoleFor, normalizeRoomRole } = require("./roles.cjs");
2
+ const { authorityEnabled, roleFromAuthority } = require("@mh-gg/authority");
3
+
4
+ function asRevokedSet(value) {
5
+ if (!value) return new Set();
6
+ if (Array.isArray(value)) return new Set(value.filter((item) => typeof item === "string" && item.length > 0));
7
+ if (value instanceof Set) return new Set([...value].filter((item) => typeof item === "string" && item.length > 0));
8
+ if (typeof value === "object") return new Set(Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([id]) => id));
9
+ return new Set();
10
+ }
11
+
12
+ function findGuest(roomState, memberId) {
13
+ const guests = roomState?.guests;
14
+ if (!guests || typeof guests !== "object") return undefined;
15
+ if (guests[memberId]) return guests[memberId];
16
+ return Object.values(guests).find((guest) => guest?.id === memberId || guest?.memberId === memberId);
17
+ }
18
+
19
+ function hasMembershipRecords(roomState) {
20
+ const members = Object.values(roomState?.members || {});
21
+ return members.some((member) => member?.profileOnly !== true) || Object.keys(roomState?.guests || {}).length > 0;
22
+ }
23
+
24
+ function evaluateRoomStateStanding({ memberId, deviceId, credentialId, actor, roomState, strict = false } = {}) {
25
+ const id = memberId || actor?.memberId;
26
+ if (typeof id !== "string" || id.length === 0) return { standing: "unknown", role: "guest" };
27
+
28
+ const revoked = asRevokedSet(roomState?.revokedCredentialIds);
29
+ if (credentialId && revoked.has(credentialId)) return { standing: "revoked", role: normalizeRoomRole(actor?.role) };
30
+
31
+ const member = roomState?.members?.[id];
32
+ const guest = findGuest(roomState, id);
33
+ if (member?.bannedAt || guest?.bannedAt) return { standing: "banned", role: normalizeRoomRole(member?.role || guest?.role || actor?.role) };
34
+
35
+ if (authorityEnabled(roomState)) {
36
+ const authorityRole = roleFromAuthority(roomState.authority, id);
37
+ if (!authorityRole) return { standing: "unknown", role: "guest", member, guest, deviceId };
38
+ return { standing: "active", role: authorityRole, member, guest, deviceId, authority: roomState.authority };
39
+ }
40
+
41
+ if (!member && !guest) {
42
+ if (strict || hasMembershipRecords(roomState)) return { standing: "unknown", role: "guest" };
43
+ return { standing: "active", role: normalizeRoomRole(actor?.role || "guest") };
44
+ }
45
+
46
+ const stateRole = normalizeRoomRole(member?.role || guest?.role || actor?.role || "guest");
47
+ return { standing: "active", role: stateRole, member, guest, deviceId };
48
+ }
49
+
50
+ function createRoomStateStandingAuthority(options = {}) {
51
+ const strict = options.strict === true;
52
+ return {
53
+ evaluate(input = {}) {
54
+ return evaluateRoomStateStanding({ ...input, strict: input.strict ?? strict });
55
+ }
56
+ };
57
+ }
58
+
59
+ function actorWithStanding(actor = {}, standing = {}) {
60
+ const stateRole = standing.role;
61
+ const keyRole = actor.keyRole || actor.role;
62
+ const role = effectiveRoleFor({ keyRole, stateRole, actorRole: actor.role });
63
+ return Object.freeze({
64
+ ...actor,
65
+ role,
66
+ keyRole,
67
+ standing: standing.standing || "unknown"
68
+ });
69
+ }
70
+
71
+ module.exports = {
72
+ actorWithStanding,
73
+ asRevokedSet,
74
+ createRoomStateStandingAuthority,
75
+ evaluateRoomStateStanding
76
+ };
package/src/shared.cjs ADDED
@@ -0,0 +1,14 @@
1
+ function clone(value) {
2
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
3
+ }
4
+
5
+ function rememberBounded(list, id, max) {
6
+ const next = Array.isArray(list) ? [...list, id] : [id];
7
+ return next.slice(-max);
8
+ }
9
+
10
+ function pruneMap(map, max) {
11
+ while (map.size > max) map.delete(map.keys().next().value);
12
+ }
13
+
14
+ module.exports = { clone, pruneMap, rememberBounded };
package/src/state.cjs ADDED
@@ -0,0 +1,54 @@
1
+ const { ROOM_STATE_SCHEMA_VERSION } = require("./constants.cjs");
2
+ const { runtimeError, MatterhornRuntimeError } = require("./errors.cjs");
3
+
4
+ function assertPlainObject(value, name) {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw runtimeError("ROOM_STATE_INVALID", `${name} must be an object`);
6
+ return value;
7
+ }
8
+
9
+ function assertString(value, name) {
10
+ if (typeof value !== "string" || value.trim() === "") throw runtimeError("ROOM_STATE_INVALID", `${name} must be a non-empty string`);
11
+ return value;
12
+ }
13
+
14
+ function assertNumber(value, name) {
15
+ if (!Number.isFinite(value)) throw runtimeError("ROOM_STATE_INVALID", `${name} must be a finite number`);
16
+ return value;
17
+ }
18
+
19
+ function parseRoomState(value) {
20
+ const state = assertPlainObject(value, "room state");
21
+ if (state.schemaVersion !== ROOM_STATE_SCHEMA_VERSION) throw runtimeError("ROOM_STATE_INVALID", `room state schemaVersion must be ${ROOM_STATE_SCHEMA_VERSION}`);
22
+ assertString(state.roomId, "room state roomId");
23
+ const appPack = assertPlainObject(state.appPack, "room state appPack");
24
+ assertString(appPack.id, "room state appPack.id");
25
+ assertString(appPack.hash, "room state appPack.hash");
26
+ if (appPack.protocolHash !== undefined) assertString(appPack.protocolHash, "room state appPack.protocolHash");
27
+ if (!Number.isInteger(state.version) || state.version < 0) throw runtimeError("ROOM_STATE_INVALID", "room state version must be a non-negative integer");
28
+ assertNumber(state.createdAt, "room state createdAt");
29
+ assertNumber(state.updatedAt, "room state updatedAt");
30
+ assertPlainObject(state.members, "room state members");
31
+ assertPlainObject(state.pluginVersions, "room state pluginVersions");
32
+ assertPlainObject(state.plugins, "room state plugins");
33
+ if (!Array.isArray(state.seenOperations)) throw runtimeError("ROOM_STATE_INVALID", "room state seenOperations must be an array");
34
+ for (const [index, operationId] of state.seenOperations.entries()) assertString(operationId, `room state seenOperations[${index}]`);
35
+ return state;
36
+ }
37
+
38
+ const RoomStateSchema = { parse: parseRoomState };
39
+
40
+ function parseWithSchema(schema, value, name) {
41
+ if (!schema || typeof schema.parse !== "function") throw runtimeError("SCHEMA_MISSING", `${name} schema is missing`);
42
+ try {
43
+ return schema.parse(value);
44
+ } catch (error) {
45
+ if (error instanceof MatterhornRuntimeError) throw error;
46
+ throw runtimeError("SCHEMA_INVALID", `${name} did not validate: ${error?.message || error}`);
47
+ }
48
+ }
49
+
50
+ module.exports = {
51
+ RoomStateSchema,
52
+ parseRoomState,
53
+ parseWithSchema
54
+ };
@@ -0,0 +1,101 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const test = require("node:test");
5
+
6
+ const {
7
+ adminIdsFromDerivedRoles,
8
+ applyAuthorityProjection,
9
+ createAuthorityState,
10
+ grantRecordMap,
11
+ grantsMatchingRevocation,
12
+ sortOperations
13
+ } = require("../src/index.cjs");
14
+ const { ensureOperationIdentity, formatHlc, validateRoomOperation } = require("@mh-gg/protocol");
15
+
16
+ function op({ memberId = "alice", pluginId = "matterhorn.core", type = "authority.grant", payload = { target: "bob", role: "admin" }, hlc, createdAt = 1000, id } = {}) {
17
+ const operation = {
18
+ ...(id ? { id } : {}),
19
+ roomId: "room",
20
+ appPackId: "app",
21
+ appPackHash: "sha256-app",
22
+ pluginId,
23
+ type,
24
+ actor: { memberId, deviceId: `dev_${memberId}`, role: "admin" },
25
+ seq: 1,
26
+ createdAt,
27
+ hlc,
28
+ payload,
29
+ auth: { credentialId: `${memberId}_cred`, signature: "sig" }
30
+ };
31
+ return id ? operation : ensureOperationIdentity(operation, { now: createdAt, nodeId: `dev_${memberId}` });
32
+ }
33
+
34
+ test("authority ordering uses HLC then bytewise content id, never createdAt or locale", () => {
35
+ const commonHlc = formatHlc({ physical: 1000, logical: 0, nodeId: "dev" });
36
+ const earlierHlc = formatHlc({ physical: 999, logical: 0, nodeId: "dev" });
37
+ const lateClock = { id: "a", hlc: commonHlc, createdAt: 1 };
38
+ const earlyClock = { id: "b", hlc: commonHlc, createdAt: 999999 };
39
+ const causalFirst = { id: "z", hlc: earlierHlc, createdAt: 999999999 };
40
+
41
+ const ordered = sortOperations([earlyClock, lateClock, causalFirst]);
42
+ assert.deepEqual(ordered.map((entry) => entry.id), ["z", "a", "b"]);
43
+
44
+ const oldLocale = process.env.LC_ALL;
45
+ process.env.LC_ALL = "tr_TR.UTF-8";
46
+ try {
47
+ assert.deepEqual(sortOperations([lateClock, causalFirst, earlyClock]).map((entry) => entry.id), ["z", "a", "b"]);
48
+ } finally {
49
+ if (oldLocale === undefined) delete process.env.LC_ALL;
50
+ else process.env.LC_ALL = oldLocale;
51
+ }
52
+
53
+ const orderingSource = fs.readFileSync(path.join(__dirname, "../../authority/src/ordering.cjs"), "utf8");
54
+ assert.equal(orderingSource.includes("localeCompare"), false);
55
+ });
56
+
57
+ test("content hash operation ids are required on room operations", () => {
58
+ const signed = op({ hlc: formatHlc({ physical: 1000, logical: 0, nodeId: "dev_alice" }) });
59
+ validateRoomOperation(signed, { now: 1000 });
60
+ assert.throws(() => validateRoomOperation({ ...signed, id: "sha256:attacker" }, { now: 1000 }), /operation.id/);
61
+ });
62
+
63
+ test("from-point authority revocation uses HLC, not signer-controlled createdAt", () => {
64
+ const beforeHlc = formatHlc({ physical: 1000, logical: 0, nodeId: "alice" });
65
+ const cutHlc = formatHlc({ physical: 1001, logical: 0, nodeId: "alice" });
66
+ const afterHlc = formatHlc({ physical: 1002, logical: 0, nodeId: "alice" });
67
+ const grantsById = new Map([
68
+ ["grant_backdated_but_after", { id: "grant_backdated_but_after", granter: "alice", target: "bob", role: "admin", createdAt: 1, hlc: afterHlc }],
69
+ ["grant_future_dated_but_before", { id: "grant_future_dated_but_before", granter: "alice", target: "carol", role: "admin", createdAt: 999999, hlc: beforeHlc }]
70
+ ]);
71
+
72
+ const matches = grantsMatchingRevocation({ target: "alice", scope: "from-point", voidFrom: cutHlc }, grantsById).map((grant) => grant.id);
73
+ assert.deepEqual(matches, ["grant_backdated_but_after"]);
74
+ });
75
+
76
+ test("adminIds is a derived projection of the authority chain", () => {
77
+ const authority = createAuthorityState({
78
+ ownerIds: ["olivia"],
79
+ grants: [
80
+ { id: "grant_alice_admin", granter: "olivia", target: "alice", role: "admin", hlc: formatHlc({ physical: 1, logical: 0, nodeId: "olivia" }) },
81
+ { id: "grant_bob_member", granter: "olivia", target: "bob", role: "member", hlc: formatHlc({ physical: 2, logical: 0, nodeId: "olivia" }) }
82
+ ]
83
+ });
84
+ const projected = applyAuthorityProjection(authority, {
85
+ olivia: { id: "olivia", role: "member", profileOnly: true, displayName: "Olivia" },
86
+ stale: { id: "stale", role: "admin" }
87
+ });
88
+ assert.deepEqual(adminIdsFromDerivedRoles(projected.authority.derivedRoles), ["alice", "olivia"]);
89
+ assert.deepEqual(projected.adminIds, ["alice", "olivia"]);
90
+ assert.equal(projected.members.olivia.profileOnly, undefined);
91
+ assert.equal(projected.members.olivia.displayName, "Olivia");
92
+ assert.equal(projected.members.stale.role, "member");
93
+ });
94
+
95
+ test("operation id tiebreak is deterministic for genuinely concurrent operations", () => {
96
+ const sharedHlc = formatHlc({ physical: 2000, logical: 0, nodeId: "dev" });
97
+ const left = op({ hlc: sharedHlc, payload: { target: "bob", role: "admin" }, createdAt: 2000 });
98
+ const right = op({ hlc: sharedHlc, payload: { target: "carol", role: "admin" }, createdAt: 1 });
99
+ const expected = [left, right].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((entry) => entry.id);
100
+ assert.deepEqual(sortOperations([right, left]).map((entry) => entry.id), expected);
101
+ });