@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,85 @@
1
+ const { DEFAULT_MAX_ACKS, DEFAULT_MAX_SEEN_OPERATIONS } = require("../constants.cjs");
2
+ const { runtimeError } = require("../errors.cjs");
3
+ const { createMemoryOperationLog, createMemoryPluginEvents, createMemoryPluginStorage, createMemoryRoomStore } = require("../memory.cjs");
4
+ const { validatePlugin } = require("../plugins/definition.cjs");
5
+ const { createRoomStateStandingAuthority } = require("../security/standingAuthority.cjs");
6
+ const { parseRoomState } = require("../state.cjs");
7
+ const { clone } = require("../shared.cjs");
8
+ const { createAuthorityState } = require("@mh-gg/authority");
9
+ const { attachRuntimeMethods } = require("./methods.cjs");
10
+ const { createRuntimeSnowflakeGenerator } = require("./snowflake.cjs");
11
+
12
+ class HostPluginRuntime {
13
+ constructor(options = {}) {
14
+ if (!options.room?.id) throw runtimeError("INVALID_ROOM", "room.id is required");
15
+ if (!options.room?.appPack?.id || !options.room?.appPack?.hash) throw runtimeError("INVALID_ROOM", "room.appPack id and hash are required");
16
+ this.room = clone(options.room);
17
+ this.hostPack = options.hostPack || null;
18
+ this.playerPacks = options.playerPacks || [];
19
+ this.operationPublicKey = options.operationPublicKey || null;
20
+ this.store = options.store || createMemoryRoomStore();
21
+ this.operationLog = options.operationLog || createMemoryOperationLog();
22
+ this.productionRuntime = options.productionRuntime === true;
23
+ this.relayExecuted = options.relayExecuted === true;
24
+ if ((this.productionRuntime || this.relayExecuted) && typeof this.store.commitStateAndOperation !== "function") {
25
+ throw runtimeError("ATOMIC_COMMIT_REQUIRED", "Production and relay-executed runtimes require store.commitStateAndOperation");
26
+ }
27
+ this.authenticateActor = options.authenticateActor || (async () => {
28
+ throw runtimeError("AUTHENTICATOR_MISSING", "authenticateActor is required");
29
+ });
30
+ this.devUnsafeTrustActorRole = options.devUnsafeTrustActorRole === true;
31
+ this.strictStanding = options.strictStanding !== undefined
32
+ ? options.strictStanding === true
33
+ : (this.productionRuntime || this.relayExecuted || this.authenticateActor.matterhornRoleKeyAuthenticator === true) && !this.devUnsafeTrustActorRole;
34
+ this.standingAuthority = options.standingAuthority || createRoomStateStandingAuthority({ strict: this.strictStanding });
35
+ this.initialMembers = options.initialMembers || {};
36
+ this.initialOwners = Array.isArray(options.initialOwners) ? options.initialOwners.slice() : [];
37
+ this.initialAuthority = options.initialAuthority || (this.initialOwners.length ? createAuthorityState({ ownerIds: this.initialOwners, grants: options.initialAuthorityGrants || [], createdAt: options.now ? options.now() : Date.now() }) : null);
38
+ this.replayBaseState = options.replayBaseState ? clone(options.replayBaseState) : null;
39
+ this.allowHistoricalAppPackHashes = options.allowHistoricalAppPackHashes === true;
40
+ this.initialRevokedCredentialIds = options.initialRevokedCredentialIds || [];
41
+ this.maxRevokedCredentialIds = Number.isInteger(options.maxRevokedCredentialIds) ? Math.max(1, options.maxRevokedCredentialIds) : 1000;
42
+ this.now = options.now || (() => Date.now());
43
+ this.snowflakes = createRuntimeSnowflakeGenerator(this, options);
44
+ this.logger = options.logger || { debug() {}, info() {}, warn() {}, error() {} };
45
+ this.capabilities = new Set(options.capabilities || []);
46
+ this.maxSeenOperations = Number.isInteger(options.maxSeenOperations) ? Math.max(1, options.maxSeenOperations) : DEFAULT_MAX_SEEN_OPERATIONS;
47
+ this.maxAcks = Number.isInteger(options.maxAckCache) ? Math.max(1, options.maxAckCache) : (Number.isInteger(options.maxAcks) ? Math.max(1, options.maxAcks) : DEFAULT_MAX_ACKS);
48
+ this.maxAckCache = Number.isInteger(options.maxAckCache) ? Math.max(1, options.maxAckCache) : this.maxAcks;
49
+ this.pluginMap = new Map();
50
+ this.acks = new Map();
51
+ this.storages = new Map();
52
+ this.eventStores = new Map();
53
+ this.started = false;
54
+
55
+ for (const plugin of options.plugins || []) {
56
+ validatePlugin(plugin);
57
+ if (this.pluginMap.has(plugin.id)) throw runtimeError("DUPLICATE_PLUGIN", `Duplicate plugin ${plugin.id}`);
58
+ this.pluginMap.set(plugin.id, plugin);
59
+ }
60
+ if (this.pluginMap.size === 0) throw runtimeError("NO_PLUGINS", "At least one plugin is required");
61
+ }
62
+
63
+ pluginList(options = {}) {
64
+ const plugins = [...this.pluginMap.values()];
65
+ return options.reverse ? plugins.reverse() : plugins;
66
+ }
67
+
68
+ validateRoomState(state) {
69
+ return parseRoomState(state);
70
+ }
71
+
72
+ pluginStorage(pluginId) {
73
+ if (!this.storages.has(pluginId)) this.storages.set(pluginId, createMemoryPluginStorage());
74
+ return this.storages.get(pluginId);
75
+ }
76
+
77
+ pluginEvents(pluginId) {
78
+ if (!this.eventStores.has(pluginId)) this.eventStores.set(pluginId, createMemoryPluginEvents());
79
+ return this.eventStores.get(pluginId);
80
+ }
81
+ }
82
+
83
+ attachRuntimeMethods(HostPluginRuntime);
84
+
85
+ module.exports = { HostPluginRuntime };
@@ -0,0 +1,146 @@
1
+ const { authorizeCoreOperation } = require("../../security/authorization/coreGate.cjs");
2
+ const {
3
+ CORE_PLUGIN_ID,
4
+ canSetRole,
5
+ grantFromOperation,
6
+ resolveAuthorityOperations,
7
+ roleFromAuthority
8
+ } = require("@mh-gg/authority");
9
+ const {
10
+ CORE_ACCESS_ROLE_ASSIGN_TYPE,
11
+ CORE_ACCESS_ROLE_DEFINE_TYPE,
12
+ CORE_SCOPE_ROLE_SET_TYPE,
13
+ applyAccessRoleAssignmentToRoomState,
14
+ applyAccessRoleDefineToRoomState,
15
+ applyScopeRoleSetToRoomState,
16
+ assertScopedRoleManagerCanApply,
17
+ normalizeScopedRolesState,
18
+ parseAccessRoleAssignmentPayload,
19
+ parseAccessRoleDefinePayload,
20
+ parseScopeRoleSetPayload
21
+ } = require("../../security/scopedRoles.cjs");
22
+ const { stateWithActorMember } = require("../memberProfiles.cjs");
23
+ const { commitVersion, projectionForState, tombstone } = require("./state.cjs");
24
+
25
+ function authorityRole(state, memberId) {
26
+ return roleFromAuthority(state.authority, memberId);
27
+ }
28
+
29
+ function applyAuthorityGrantDuringReplay(runtime, state, operation, voidedGrantIds) {
30
+ const grant = grantFromOperation(operation);
31
+ if (!grant) {
32
+ tombstone(state, operation, "invalid-authority-grant");
33
+ return false;
34
+ }
35
+ if (voidedGrantIds.has(grant.id)) {
36
+ tombstone(state, operation, "voided-by-revocation");
37
+ return false;
38
+ }
39
+ if (!canSetRole(authorityRole(state, grant.granter), grant.role, authorityRole(state, grant.target))) {
40
+ tombstone(state, operation, "unauthorized-authority-grant", { granter: grant.granter, target: grant.target, role: grant.role });
41
+ return false;
42
+ }
43
+ state.authority.grants = [...(state.authority.grants || []), grant];
44
+ projectionForState(state);
45
+ Object.assign(state, stateWithActorMember(state, operation.actor, operation.createdAt || runtime.now()));
46
+ commitVersion(state, operation, runtime);
47
+ return true;
48
+ }
49
+
50
+ function applyAuthorityRevokeDuringReplay(runtime, state, operation, validRevocationIds) {
51
+ if (!validRevocationIds.has(operation.id)) {
52
+ tombstone(state, operation, "unauthorized-authority-revocation");
53
+ return false;
54
+ }
55
+ const resolved = resolveAuthorityOperations({ authority: state.authority, operations: [operation] });
56
+ state.authority.revocations = resolved.revocations;
57
+ state.authority.tombstones = { ...(state.authority.tombstones || {}), ...(resolved.tombstones || {}) };
58
+ projectionForState(state);
59
+ Object.assign(state, stateWithActorMember(state, operation.actor, operation.createdAt || runtime.now()));
60
+ commitVersion(state, operation, runtime);
61
+ return true;
62
+ }
63
+
64
+ async function applyCredentialRevocationDuringReplay(runtime, state, operation, actor = operation.actor) {
65
+ const credentialId = operation.payload?.credentialId;
66
+ if (typeof credentialId !== "string" || credentialId.length === 0) {
67
+ tombstone(state, operation, "invalid-credential-revocation");
68
+ return false;
69
+ }
70
+ const revoked = Array.isArray(state.revokedCredentialIds) ? state.revokedCredentialIds.filter((id) => id !== credentialId) : [];
71
+ revoked.push(credentialId);
72
+ state.revokedCredentialIds = revoked.slice(-runtime.maxRevokedCredentialIds);
73
+ Object.assign(state, stateWithActorMember(state, actor, operation.createdAt || runtime.now()));
74
+ commitVersion(state, operation, runtime);
75
+ return true;
76
+ }
77
+
78
+ async function applyCredentialRevocationEnvelope(runtime, state, operation) {
79
+ try {
80
+ const rawActor = await runtime.authenticateActor(operation.auth, operation.actor, operation);
81
+ const actor = await authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin: { id: CORE_PLUGIN_ID }, roles: ["admin"], operationLabel: `${CORE_PLUGIN_ID}.${operation.type}` });
82
+ return applyCredentialRevocationDuringReplay(runtime, state, operation, actor);
83
+ } catch (error) {
84
+ tombstone(state, operation, "unauthorized-credential-revocation", { code: error?.code, message: error?.message || String(error) });
85
+ return false;
86
+ }
87
+ }
88
+
89
+ async function applyScopeRoleSetEnvelope(runtime, state, operation) {
90
+ try {
91
+ const rawActor = await runtime.authenticateActor(operation.auth, operation.actor, operation);
92
+ const actor = await authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin: { id: CORE_PLUGIN_ID }, roles: ["moderator"], operationLabel: `${CORE_PLUGIN_ID}.${CORE_SCOPE_ROLE_SET_TYPE}` });
93
+ const payload = parseScopeRoleSetPayload(operation);
94
+ assertScopedRoleManagerCanApply(actor, { grants: [{ role: payload.role || payload.defaultRole || "none" }] });
95
+ const updatedAt = operation.createdAt || runtime.now();
96
+ Object.assign(state, stateWithActorMember(applyScopeRoleSetToRoomState(state, payload, actor, updatedAt, operation), actor, updatedAt));
97
+ commitVersion(state, operation, runtime);
98
+ return true;
99
+ } catch (error) {
100
+ tombstone(state, operation, "unauthorized-scope-role-set", { code: error?.code, message: error?.message || String(error) });
101
+ return false;
102
+ }
103
+ }
104
+
105
+ async function applyAccessRoleDefineEnvelope(runtime, state, operation) {
106
+ try {
107
+ const rawActor = await runtime.authenticateActor(operation.auth, operation.actor, operation);
108
+ const actor = await authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin: { id: CORE_PLUGIN_ID }, roles: ["moderator"], operationLabel: `${CORE_PLUGIN_ID}.${CORE_ACCESS_ROLE_DEFINE_TYPE}` });
109
+ const payload = parseAccessRoleDefinePayload(operation);
110
+ assertScopedRoleManagerCanApply(actor, payload);
111
+ const updatedAt = operation.createdAt || runtime.now();
112
+ Object.assign(state, stateWithActorMember(applyAccessRoleDefineToRoomState(state, payload, actor, updatedAt, operation), actor, updatedAt));
113
+ commitVersion(state, operation, runtime);
114
+ return true;
115
+ } catch (error) {
116
+ tombstone(state, operation, "unauthorized-access-role-define", { code: error?.code, message: error?.message || String(error) });
117
+ return false;
118
+ }
119
+ }
120
+
121
+ async function applyAccessRoleAssignmentEnvelope(runtime, state, operation) {
122
+ try {
123
+ const rawActor = await runtime.authenticateActor(operation.auth, operation.actor, operation);
124
+ const actor = await authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin: { id: CORE_PLUGIN_ID }, roles: ["moderator"], operationLabel: `${CORE_PLUGIN_ID}.${operation.type}` });
125
+ const payload = parseAccessRoleAssignmentPayload(operation);
126
+ const compoundRole = normalizeScopedRolesState(state.scopedRoles).roles[payload.roleId];
127
+ if (!compoundRole) throw new Error(`Compound role ${payload.roleId} is not defined`);
128
+ assertScopedRoleManagerCanApply(actor, { grants: compoundRole.grants });
129
+ const updatedAt = operation.createdAt || runtime.now();
130
+ Object.assign(state, stateWithActorMember(applyAccessRoleAssignmentToRoomState(state, payload, operation.type === CORE_ACCESS_ROLE_ASSIGN_TYPE, actor, updatedAt, operation), actor, updatedAt));
131
+ commitVersion(state, operation, runtime);
132
+ return true;
133
+ } catch (error) {
134
+ tombstone(state, operation, "unauthorized-access-role-assignment", { code: error?.code, message: error?.message || String(error) });
135
+ return false;
136
+ }
137
+ }
138
+
139
+ module.exports = {
140
+ applyAccessRoleAssignmentEnvelope,
141
+ applyAccessRoleDefineEnvelope,
142
+ applyAuthorityGrantDuringReplay,
143
+ applyAuthorityRevokeDuringReplay,
144
+ applyCredentialRevocationEnvelope,
145
+ applyScopeRoleSetEnvelope
146
+ };
@@ -0,0 +1,49 @@
1
+ const { authorizeCoreOperation } = require("../../security/authorization/coreGate.cjs");
2
+ const { parseWithSchema } = require("../../state.cjs");
3
+ const { clone } = require("../../shared.cjs");
4
+ const { stateWithActorMember } = require("../memberProfiles.cjs");
5
+ const { commitVersion, tombstone } = require("./state.cjs");
6
+
7
+ async function applyContentDuringReplay(runtime, state, operation) {
8
+ const plugin = runtime.pluginMap.get(operation.pluginId);
9
+ if (!plugin) {
10
+ tombstone(state, operation, "plugin-not-installed");
11
+ return false;
12
+ }
13
+ const operationSchema = plugin.schemas.operations[operation.type];
14
+ if (!operationSchema) {
15
+ tombstone(state, operation, "unknown-operation-type");
16
+ return false;
17
+ }
18
+ let actor;
19
+ try {
20
+ const rawActor = await runtime.authenticateActor(operation.auth, operation.actor, operation);
21
+ actor = await authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin });
22
+ } catch (error) {
23
+ tombstone(state, operation, "unauthorized-operation", { code: error?.code, message: error?.message || String(error) });
24
+ return false;
25
+ }
26
+
27
+ let parsedPayload;
28
+ try {
29
+ parsedPayload = parseWithSchema(operationSchema, operation.payload, `${plugin.id}.${operation.type}`);
30
+ } catch (error) {
31
+ tombstone(state, operation, "invalid-payload", { code: error?.code, message: error?.message || String(error) });
32
+ return false;
33
+ }
34
+ const parsedOperation = { ...operation, payload: parsedPayload, actor };
35
+ const pluginState = state.plugins[plugin.id];
36
+ const authz = await plugin.authorize(runtime.createContext({ plugin, roomState: state, pluginState, actor, operation: parsedOperation }), parsedOperation);
37
+ if (!authz?.ok) {
38
+ tombstone(state, operation, "plugin-denied", { message: authz?.reason || "Forbidden" });
39
+ return false;
40
+ }
41
+ const nextPluginState = await plugin.reduce(runtime.createContext({ plugin, roomState: state, pluginState, actor, operation: parsedOperation }), clone(pluginState), parsedOperation);
42
+ state.plugins[plugin.id] = parseWithSchema(plugin.schemas.state, nextPluginState, `${plugin.id} state`);
43
+ state.pluginVersions[plugin.id] = plugin.version;
44
+ Object.assign(state, stateWithActorMember(state, actor, operation.createdAt || runtime.now()));
45
+ commitVersion(state, operation, runtime);
46
+ return true;
47
+ }
48
+
49
+ module.exports = { applyContentDuringReplay };
@@ -0,0 +1,70 @@
1
+ const { clone } = require("../../shared.cjs");
2
+ const {
3
+ CORE_PLUGIN_ID,
4
+ CORE_REVOKE_CREDENTIAL_TYPE,
5
+ computeVoidedGrantIds,
6
+ createAuthorityState,
7
+ isAuthorityGrantOperation,
8
+ isAuthorityRevokeOperation,
9
+ normalizeAuthorityState,
10
+ sortOperations
11
+ } = require("@mh-gg/authority");
12
+ const { CORE_ACCESS_ROLE_ASSIGN_TYPE, CORE_ACCESS_ROLE_DEFINE_TYPE, CORE_ACCESS_ROLE_UNASSIGN_TYPE, CORE_SCOPE_ROLE_SET_TYPE } = require("../../security/scopedRoles.cjs");
13
+ const {
14
+ applyAccessRoleAssignmentEnvelope,
15
+ applyAccessRoleDefineEnvelope,
16
+ applyAuthorityGrantDuringReplay,
17
+ applyAuthorityRevokeDuringReplay,
18
+ applyCredentialRevocationEnvelope,
19
+ applyScopeRoleSetEnvelope
20
+ } = require("./applyAuthority.cjs");
21
+ const { applyContentDuringReplay } = require("./applyContent.cjs");
22
+ const { applyDirectCoreEnvelope, isDirectCoreOperation } = require("../directMessages.cjs");
23
+ const { CORE_READ_TAG_SET_TYPE, applyReadTagCoreEnvelope } = require("../readTags.cjs");
24
+ const { normalizeOperationForReplay, projectionForState, resetReplayState, tombstone } = require("./state.cjs");
25
+
26
+ function isCoreOperation(operation) {
27
+ return operation?.pluginId === CORE_PLUGIN_ID;
28
+ }
29
+
30
+ function removeVoidedBaseGrants(state, voidedGrantIds) {
31
+ if (!state.authority?.grants) return;
32
+ state.authority.grants = state.authority.grants.filter((grant) => {
33
+ const keep = !voidedGrantIds.has(grant.id);
34
+ if (!keep) tombstone(state, { id: grant.id, type: "authority.grant", pluginId: CORE_PLUGIN_ID, actor: { memberId: grant.granter }, createdAt: grant.createdAt }, "voided-by-revocation", { source: "base" });
35
+ return keep;
36
+ });
37
+ }
38
+
39
+ async function applyReplayOperation(runtime, state, operation, voidedGrantIds, validRevocationIds) {
40
+ if (isAuthorityGrantOperation(operation)) return applyAuthorityGrantDuringReplay(runtime, state, operation, voidedGrantIds);
41
+ if (isAuthorityRevokeOperation(operation)) return applyAuthorityRevokeDuringReplay(runtime, state, operation, validRevocationIds);
42
+ if (isCoreOperation(operation) && operation.type === CORE_REVOKE_CREDENTIAL_TYPE) return applyCredentialRevocationEnvelope(runtime, state, operation);
43
+ if (isCoreOperation(operation) && operation.type === CORE_SCOPE_ROLE_SET_TYPE) return applyScopeRoleSetEnvelope(runtime, state, operation);
44
+ if (isCoreOperation(operation) && operation.type === CORE_ACCESS_ROLE_DEFINE_TYPE) return applyAccessRoleDefineEnvelope(runtime, state, operation);
45
+ if (isCoreOperation(operation) && (operation.type === CORE_ACCESS_ROLE_ASSIGN_TYPE || operation.type === CORE_ACCESS_ROLE_UNASSIGN_TYPE)) return applyAccessRoleAssignmentEnvelope(runtime, state, operation);
46
+ if (isDirectCoreOperation(operation)) return applyDirectCoreEnvelope(runtime, state, operation);
47
+ if (isCoreOperation(operation) && operation.type === CORE_READ_TAG_SET_TYPE) return applyReadTagCoreEnvelope(runtime, state, operation);
48
+ return applyContentDuringReplay(runtime, state, operation);
49
+ }
50
+
51
+ async function rebuildStateFromAuthorityLog(runtime, baseState, operations = []) {
52
+ const sorted = sortOperations(operations.map(clone));
53
+ const authority = normalizeAuthorityState(baseState.authority) || createAuthorityState({ ownerIds: [] });
54
+ const { voidedGrantIds, validRevocationIds } = computeVoidedGrantIds(authority, sorted);
55
+ const state = resetReplayState({ ...baseState, authority });
56
+ removeVoidedBaseGrants(state, voidedGrantIds);
57
+ projectionForState(state);
58
+
59
+ for (const original of sorted) {
60
+ try {
61
+ await applyReplayOperation(runtime, state, normalizeOperationForReplay(runtime, original), voidedGrantIds, validRevocationIds);
62
+ } catch (error) {
63
+ tombstone(state, original, "invalid-operation", { code: error?.code, message: error?.message || String(error) });
64
+ }
65
+ }
66
+ projectionForState(state);
67
+ return runtime.validateRoomState(state);
68
+ }
69
+
70
+ module.exports = { rebuildStateFromAuthorityLog };
@@ -0,0 +1,56 @@
1
+ const { validateRoomOperation } = require("@mh-gg/protocol");
2
+ const { runtimeError } = require("../../errors.cjs");
3
+ const { clone, rememberBounded } = require("../../shared.cjs");
4
+ const { applyAuthorityProjection, normalizeAuthorityState } = require("@mh-gg/authority");
5
+
6
+ function resetReplayState(baseState) {
7
+ return {
8
+ ...clone(baseState),
9
+ version: 0,
10
+ updatedAt: baseState.createdAt,
11
+ seenOperations: [],
12
+ revokedCredentialIds: Array.isArray(baseState.revokedCredentialIds) ? baseState.revokedCredentialIds.slice() : [],
13
+ plugins: clone(baseState.plugins || {}),
14
+ pluginVersions: clone(baseState.pluginVersions || {}),
15
+ members: clone(baseState.members || {}),
16
+ authority: normalizeAuthorityState(baseState.authority)
17
+ };
18
+ }
19
+
20
+ function normalizeOperationForReplay(runtime, operation) {
21
+ validateRoomOperation(operation);
22
+ if (operation.roomId !== runtime.room.id) throw runtimeError("ROOM_MISMATCH", "Operation room does not match runtime room");
23
+ if (operation.appPackId !== runtime.room.appPack.id) throw runtimeError("APP_PACK_MISMATCH", "Operation app pack id does not match runtime app");
24
+ if (operation.appPackHash !== runtime.room.appPack.hash) throw runtimeError("APP_PACK_HASH_MISMATCH", "Operation app pack hash does not match runtime app");
25
+ return operation;
26
+ }
27
+
28
+ function tombstone(state, operation, reason, details = {}) {
29
+ if (!state.authority) return;
30
+ state.authority.tombstones = { ...(state.authority.tombstones || {}) };
31
+ if (!state.authority.tombstones[operation.id]) {
32
+ state.authority.tombstones[operation.id] = {
33
+ reason,
34
+ operationType: operation.type,
35
+ pluginId: operation.pluginId,
36
+ actor: operation.actor?.memberId,
37
+ at: operation.createdAt,
38
+ ...details
39
+ };
40
+ }
41
+ }
42
+
43
+ function commitVersion(state, operation, runtime) {
44
+ state.version += 1;
45
+ state.updatedAt = operation.createdAt || runtime.now();
46
+ state.seenOperations = rememberBounded(state.seenOperations, operation.id, runtime.maxSeenOperations);
47
+ }
48
+
49
+ function projectionForState(state) {
50
+ const projected = applyAuthorityProjection(state.authority, state.members);
51
+ state.authority = projected.authority;
52
+ state.members = projected.members;
53
+ state.adminIds = projected.adminIds;
54
+ }
55
+
56
+ module.exports = { commitVersion, normalizeOperationForReplay, projectionForState, resetReplayState, tombstone };
@@ -0,0 +1,65 @@
1
+ const { hashCanonical } = require("@mh-gg/base");
2
+ const { runtimeError } = require("../errors.cjs");
3
+ const { clone } = require("../shared.cjs");
4
+ const { canEditScope, canViewScope, scopedRoleForActor } = require("../security/scopedRoles.cjs");
5
+ const { entityIdForOperation } = require("./snowflake.cjs");
6
+
7
+ function createContext({ plugin, roomState, pluginState, actor, operation }) {
8
+ const runtime = this;
9
+ let randomIndex = 0;
10
+ return {
11
+ room: {
12
+ id: this.room.id,
13
+ appPackId: this.room.appPack.id,
14
+ appPackHash: this.room.appPack.hash
15
+ },
16
+ plugin: { id: plugin.id, version: plugin.version, ...(plugin.config === undefined ? {} : { config: clone(plugin.config) }) },
17
+ actor,
18
+ logger: this.logger,
19
+ roomState: clone(roomState),
20
+ pluginState: clone(pluginState),
21
+ now: operation?.createdAt ?? this.now(),
22
+ crypto: {
23
+ hash: hashCanonical,
24
+ verifySignature() { return false; },
25
+ randomId(prefix = "id") {
26
+ randomIndex += 1;
27
+ if (operation?.ledgerId || operation?.snowflakeId || operation?.createdAt) return entityIdForOperation(prefix, operation, randomIndex > 1 ? randomIndex : 0);
28
+ const digest = hashCanonical({ operationId: operation?.id || "initial", pluginId: plugin.id, prefix, randomIndex })
29
+ .slice("sha256-".length, "sha256-".length + 16);
30
+ return `${prefix}_${digest}`;
31
+ }
32
+ },
33
+ storage: this.pluginStorage(plugin.id),
34
+ events: this.pluginEvents(plugin.id),
35
+ access: {
36
+ roleForScope(scopeType, scopeId, inputActor = actor) {
37
+ return scopedRoleForActor(roomState, inputActor, scopeType, scopeId);
38
+ },
39
+ canView(scopeType, scopeId, inputActor = actor) {
40
+ return canViewScope(roomState, inputActor, scopeType, scopeId);
41
+ },
42
+ canEdit(scopeType, scopeId, inputActor = actor) {
43
+ return canEditScope(roomState, inputActor, scopeType, scopeId);
44
+ }
45
+ },
46
+ plugins: {
47
+ has(pluginId) { return runtime.pluginMap.has(pluginId); },
48
+ async call(pluginId, method, input) {
49
+ const target = runtime.pluginMap.get(pluginId);
50
+ if (!target) throw runtimeError("PLUGIN_NOT_INSTALLED", `Plugin ${pluginId} is not installed`);
51
+ const fn = target.methods?.[method];
52
+ if (typeof fn !== "function") throw runtimeError("PLUGIN_METHOD_NOT_FOUND", `Plugin method ${pluginId}.${method} is not available`);
53
+ return await fn({ actor: clone(actor), roomState: clone(roomState), state: clone(roomState.plugins?.[pluginId]), capabilities: runtime.capabilities }, clone(input));
54
+ }
55
+ },
56
+ capabilities: {
57
+ has: (capability) => this.capabilities.has(capability),
58
+ require: (capability) => {
59
+ if (!this.capabilities.has(capability)) throw runtimeError("CAPABILITY_MISSING", `Missing capability ${capability}`);
60
+ }
61
+ }
62
+ };
63
+ }
64
+
65
+ module.exports = { createContext };
@@ -0,0 +1,169 @@
1
+ const { authorizeCoreOperation } = require("../security/authorization/coreGate.cjs");
2
+ const { runtimeError } = require("../errors.cjs");
3
+ const { clone, pruneMap, rememberBounded } = require("../shared.cjs");
4
+ const { rebuildStateFromAuthorityLog } = require("./authorityReplay/index.cjs");
5
+ const {
6
+ CORE_AUTHORITY_GRANT_TYPE,
7
+ CORE_AUTHORITY_REVOKE_TYPE,
8
+ CORE_PLUGIN_ID,
9
+ CORE_REVOKE_CREDENTIAL_TYPE,
10
+ canSetRole,
11
+ roleFromAuthority
12
+ } = require("@mh-gg/authority");
13
+ const { stateWithActorMember } = require("./memberProfiles.cjs");
14
+ const {
15
+ CORE_ACCESS_ROLE_ASSIGN_TYPE,
16
+ CORE_ACCESS_ROLE_DEFINE_TYPE,
17
+ CORE_ACCESS_ROLE_UNASSIGN_TYPE,
18
+ CORE_SCOPE_ROLE_SET_TYPE,
19
+ } = require("../security/scopedRoles.cjs");
20
+ const {
21
+ commitAccessRoleAssignment,
22
+ commitAccessRoleDefine,
23
+ commitScopeRoleSet
24
+ } = require("./scopedRoleOperations.cjs");
25
+ const {
26
+ CORE_DIRECT_MESSAGE_SEND_TYPE,
27
+ CORE_DIRECT_MESSAGE_PUBLISH_TYPE,
28
+ CORE_DM_MESSAGE_TYPE,
29
+ CORE_MEMBER_KEY_PUBLISH_TYPE,
30
+ commitDirectCoreOperation,
31
+ isDirectCoreOperation,
32
+ } = require("./directMessages.cjs");
33
+ const { CORE_READ_TAG_SET_TYPE, applyReadTagToRoomState, parseReadTagPayload } = require("./readTags.cjs");
34
+ const {
35
+ parseAuthorityGrantPayload,
36
+ parseAuthorityRevokePayload,
37
+ parseCorePayload,
38
+ parseCredentialRevokePayload
39
+ } = require("./corePayloads.cjs");
40
+
41
+ const CORE_OPERATION_ROLES = Object.freeze({
42
+ [CORE_REVOKE_CREDENTIAL_TYPE]: ["admin"],
43
+ [CORE_AUTHORITY_GRANT_TYPE]: ["admin"],
44
+ [CORE_AUTHORITY_REVOKE_TYPE]: ["admin"],
45
+ [CORE_SCOPE_ROLE_SET_TYPE]: ["moderator"],
46
+ [CORE_ACCESS_ROLE_DEFINE_TYPE]: ["moderator"],
47
+ [CORE_ACCESS_ROLE_ASSIGN_TYPE]: ["moderator"],
48
+ [CORE_ACCESS_ROLE_UNASSIGN_TYPE]: ["moderator"],
49
+ [CORE_MEMBER_KEY_PUBLISH_TYPE]: ["member"],
50
+ [CORE_DIRECT_MESSAGE_PUBLISH_TYPE]: ["member"],
51
+ [CORE_DIRECT_MESSAGE_SEND_TYPE]: ["member"],
52
+ [CORE_DM_MESSAGE_TYPE]: ["member"],
53
+ [CORE_READ_TAG_SET_TYPE]: ["member"]
54
+ });
55
+ function isCoreOperation(operation) {
56
+ return operation?.pluginId === CORE_PLUGIN_ID;
57
+ }
58
+
59
+ function revokedCredentialList(state) {
60
+ if (Array.isArray(state.revokedCredentialIds)) return state.revokedCredentialIds.slice();
61
+ if (state.revokedCredentialIds && typeof state.revokedCredentialIds === "object") return Object.entries(state.revokedCredentialIds).filter(([, enabled]) => Boolean(enabled)).map(([credentialId]) => credentialId);
62
+ return [];
63
+ }
64
+ async function operationLogEntries(runtime) {
65
+ if (typeof runtime.operationLog.list === "function") return await runtime.operationLog.list();
66
+ return runtime.operationLog.entries || [];
67
+ }
68
+ async function commitAuthorityCoreOperation(operation, actor, state, signedOperationForLog) {
69
+ const payload = parseCorePayload(operation);
70
+ if (operation.type === CORE_AUTHORITY_GRANT_TYPE && !canSetRole(actor.role, payload.role, roleFromAuthority(state.authority, payload.target))) {
71
+ throw runtimeError("FORBIDDEN", `authority.grant ${payload.target} -> ${payload.role} exceeds actor authority ${actor.role}`);
72
+ }
73
+ const parsedOperation = { ...operation, payload, actor };
74
+ const createdAt = parsedOperation.createdAt || this.now();
75
+ const logEntry = { ...signedOperationForLog, payload, committedAt: createdAt };
76
+ const rebuilt = await rebuildStateFromAuthorityLog(this, this.replayBaseState || state, [...(await operationLogEntries(this)), logEntry]);
77
+ const memberState = stateWithActorMember(rebuilt, actor, createdAt);
78
+ const nextRoomState = this.validateRoomState({ ...memberState, updatedAt: createdAt, seenOperations: rememberBounded(memberState.seenOperations, parsedOperation.id, this.maxSeenOperations) });
79
+ await this.commitStateAndOperation({
80
+ logEntry: { ...logEntry, committedRoomVersion: nextRoomState.version, committedAt: nextRoomState.updatedAt },
81
+ nextState: nextRoomState
82
+ });
83
+ const ack = { ok: true, acceptedOperationId: parsedOperation.id, roomVersion: nextRoomState.version, ...(parsedOperation.ledgerId ? { acceptedLedgerId: parsedOperation.ledgerId, acceptedSnowflakeId: parsedOperation.ledgerId } : {}) };
84
+ this.acks.set(parsedOperation.id, ack);
85
+ pruneMap(this.acks, Math.min(this.maxAcks, this.maxAckCache));
86
+ return clone(ack);
87
+ }
88
+
89
+ async function commitCredentialRevocation(operation, actor, state, signedOperationForLog) {
90
+ const payload = parseCorePayload(operation);
91
+ const parsedOperation = { ...operation, payload, actor };
92
+ const revoked = revokedCredentialList(state).filter((credentialId) => credentialId !== payload.credentialId);
93
+ revoked.push(payload.credentialId);
94
+ const updatedAt = parsedOperation.createdAt || this.now();
95
+ const memberState = stateWithActorMember(state, actor, updatedAt);
96
+ const nextRoomState = this.validateRoomState({ ...memberState, revokedCredentialIds: revoked.slice(-this.maxRevokedCredentialIds), version: state.version + 1, updatedAt, seenOperations: rememberBounded(state.seenOperations, parsedOperation.id, this.maxSeenOperations) });
97
+ await this.commitStateAndOperation({
98
+ logEntry: { ...signedOperationForLog, committedRoomVersion: nextRoomState.version, committedAt: nextRoomState.updatedAt },
99
+ nextState: nextRoomState,
100
+ expectedPreviousVersion: state.version
101
+ });
102
+ const ack = { ok: true, acceptedOperationId: parsedOperation.id, roomVersion: nextRoomState.version, ...(parsedOperation.ledgerId ? { acceptedLedgerId: parsedOperation.ledgerId, acceptedSnowflakeId: parsedOperation.ledgerId } : {}) };
103
+ this.acks.set(parsedOperation.id, ack);
104
+ pruneMap(this.acks, Math.min(this.maxAcks, this.maxAckCache));
105
+ return clone(ack);
106
+ }
107
+
108
+ async function commitReadTagCoreOperation(operation, actor, state, signedOperationForLog) {
109
+ const payload = parseReadTagPayload(operation);
110
+ const parsedOperation = { ...operation, payload, actor };
111
+ const updatedAt = parsedOperation.createdAt || this.now();
112
+ const memberState = stateWithActorMember(state, actor, updatedAt);
113
+ const readState = applyReadTagToRoomState(memberState, payload, actor, parsedOperation, updatedAt);
114
+ const nextRoomState = this.validateRoomState({
115
+ ...readState,
116
+ version: state.version + 1,
117
+ updatedAt,
118
+ seenOperations: rememberBounded(state.seenOperations, parsedOperation.id, this.maxSeenOperations)
119
+ });
120
+ await this.commitStateAndOperation({
121
+ logEntry: { ...signedOperationForLog, payload, committedRoomVersion: nextRoomState.version, committedAt: nextRoomState.updatedAt },
122
+ nextState: nextRoomState,
123
+ expectedPreviousVersion: state.version
124
+ });
125
+ const ack = { ok: true, acceptedOperationId: parsedOperation.id, roomVersion: nextRoomState.version, ...(parsedOperation.ledgerId ? { acceptedLedgerId: parsedOperation.ledgerId, acceptedSnowflakeId: parsedOperation.ledgerId } : {}) };
126
+ this.acks.set(parsedOperation.id, ack);
127
+ pruneMap(this.acks, Math.min(this.maxAcks, this.maxAckCache));
128
+ return clone(ack);
129
+ }
130
+
131
+ async function commitCoreOperation(operation, actor, state, signedOperationForLog) {
132
+ if (operation.type === CORE_AUTHORITY_GRANT_TYPE || operation.type === CORE_AUTHORITY_REVOKE_TYPE) return this.commitAuthorityCoreOperation(operation, actor, state, signedOperationForLog);
133
+ if (operation.type === CORE_SCOPE_ROLE_SET_TYPE) return this.commitScopeRoleSet(operation, actor, state, signedOperationForLog);
134
+ if (operation.type === CORE_ACCESS_ROLE_DEFINE_TYPE) return this.commitAccessRoleDefine(operation, actor, state, signedOperationForLog);
135
+ if (operation.type === CORE_ACCESS_ROLE_ASSIGN_TYPE || operation.type === CORE_ACCESS_ROLE_UNASSIGN_TYPE) return this.commitAccessRoleAssignment(operation, actor, state, signedOperationForLog);
136
+ if (isDirectCoreOperation(operation)) return this.commitDirectCoreOperation(operation, actor, state, signedOperationForLog);
137
+ if (operation.type === CORE_READ_TAG_SET_TYPE) return this.commitReadTagCoreOperation(operation, actor, state, signedOperationForLog);
138
+ return this.commitCredentialRevocation(operation, actor, state, signedOperationForLog);
139
+ }
140
+
141
+ async function authenticateAndGateOperation(runtime, { operation, state, plugin }) {
142
+ const rawActor = await runtime.authenticateActor(operation.auth, operation.actor, operation);
143
+ if (!isCoreOperation(operation)) return authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin });
144
+ return authorizeCoreOperation(runtime, { operation, actor: rawActor, state, plugin: { id: CORE_PLUGIN_ID }, roles: CORE_OPERATION_ROLES[operation.type], operationLabel: `${CORE_PLUGIN_ID}.${operation.type}` });
145
+ }
146
+
147
+ module.exports = {
148
+ CORE_ACCESS_ROLE_ASSIGN_TYPE,
149
+ CORE_ACCESS_ROLE_DEFINE_TYPE,
150
+ CORE_ACCESS_ROLE_UNASSIGN_TYPE,
151
+ CORE_AUTHORITY_GRANT_TYPE,
152
+ CORE_AUTHORITY_REVOKE_TYPE,
153
+ CORE_PLUGIN_ID,
154
+ CORE_READ_TAG_SET_TYPE,
155
+ CORE_REVOKE_CREDENTIAL_TYPE,
156
+ CORE_SCOPE_ROLE_SET_TYPE,
157
+ authenticateAndGateOperation,
158
+ commitAccessRoleAssignment,
159
+ commitAccessRoleDefine,
160
+ commitAuthorityCoreOperation,
161
+ commitCoreOperation,
162
+ commitCredentialRevocation,
163
+ commitDirectCoreOperation,
164
+ commitReadTagCoreOperation,
165
+ commitScopeRoleSet,
166
+ isCoreOperation,
167
+ isDirectCoreOperation,
168
+ parseCorePayload
169
+ };