@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,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
|
+
};
|