@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
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@mh-gg/host-runtime",
3
+ "version": "0.1.1-alpha.20260613T085325975Z",
4
+ "description": "Authoritative Matterhorn host plugin runtime.",
5
+ "type": "commonjs",
6
+ "main": "src/index.cjs",
7
+ "exports": {
8
+ ".": "./src/index.cjs"
9
+ },
10
+ "dependencies": {
11
+ "@mh-gg/event": "^0.1.1-alpha.20260613T085325975Z",
12
+ "@mh-gg/authority": "^0.1.1-alpha.20260603T124500153Z",
13
+ "@mh-gg/base": "^0.1.1-alpha.20260613T085325975Z",
14
+ "@mh-gg/protocol": "^0.1.1-alpha.20260613T085325975Z"
15
+ },
16
+ "engines": {
17
+ "node": ">=22.12"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test test/*.test.cjs",
21
+ "coverage": "node --test --experimental-test-coverage --test-coverage-lines=80 --test-coverage-functions=80 --test-coverage-branches=75 --test-coverage-include=src/**/*.cjs test/*.test.cjs"
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ const ROOM_STATE_SCHEMA_VERSION = 1;
2
+ const DEFAULT_MAX_SEEN_OPERATIONS = 500;
3
+ const DEFAULT_MAX_ACKS = 500;
4
+
5
+ module.exports = {
6
+ DEFAULT_MAX_ACKS,
7
+ DEFAULT_MAX_SEEN_OPERATIONS,
8
+ ROOM_STATE_SCHEMA_VERSION
9
+ };
package/src/errors.cjs ADDED
@@ -0,0 +1,39 @@
1
+ class MatterhornRuntimeError extends Error {
2
+ constructor(code, message) {
3
+ super(message || code);
4
+ this.name = "MatterhornRuntimeError";
5
+ this.code = code;
6
+ }
7
+ }
8
+
9
+ function runtimeError(code, message) {
10
+ return new MatterhornRuntimeError(code, message);
11
+ }
12
+
13
+ function allow(details = {}) {
14
+ return { ok: true, ...details };
15
+ }
16
+
17
+ function deny(reason = "Forbidden") {
18
+ return { ok: false, reason };
19
+ }
20
+
21
+ function normalizeRuntimeFailure(error, operationId) {
22
+ if (error instanceof MatterhornRuntimeError) {
23
+ return { ok: false, code: error.code, reason: error.message, ...(operationId ? { operationId } : {}) };
24
+ }
25
+ return {
26
+ ok: false,
27
+ code: "PLUGIN_FAILURE",
28
+ reason: error?.message || String(error || "Plugin failed"),
29
+ ...(operationId ? { operationId } : {})
30
+ };
31
+ }
32
+
33
+ module.exports = {
34
+ MatterhornRuntimeError,
35
+ allow,
36
+ deny,
37
+ normalizeRuntimeFailure,
38
+ runtimeError
39
+ };
@@ -0,0 +1,35 @@
1
+ const {
2
+ assertProductionPluginGates,
3
+ assertTrustedManifest,
4
+ loadPackReference,
5
+ resolvePluginGraph,
6
+ validateAppPackManifest,
7
+ validateHostPackManifest
8
+ } = require("@mh-gg/base");
9
+ const { runtimeError } = require("../errors.cjs");
10
+ const { installHostPack } = require("../plugins/install.cjs");
11
+
12
+ async function installAppPack(appPackRef, options = {}) {
13
+ const appPack = validateAppPackManifest(await loadPackReference(appPackRef, options));
14
+ if (options.trustStore) assertTrustedManifest(appPack, options.trustStore, { requireSignature: options.requireSignature });
15
+ const hostPack = validateHostPackManifest(await loadPackReference(appPack.hostPack, options));
16
+ const installed = installHostPack({ appPack, hostPack, pluginRegistry: options.pluginRegistry });
17
+ const graph = resolvePluginGraph(installed.plugins);
18
+ if (hostPack.compatibility.pluginGraphHash !== graph.pluginGraphHash && options.skipPluginGraphHash !== true) {
19
+ throw runtimeError("PLUGIN_GRAPH_HASH_MISMATCH", `Host pack plugin graph hash mismatch: expected ${hostPack.compatibility.pluginGraphHash}, got ${graph.pluginGraphHash}`);
20
+ }
21
+ if (options.production) {
22
+ assertProductionPluginGates({
23
+ hostPack,
24
+ publisher: appPack.publisher,
25
+ trustStore: options.trustStore,
26
+ requireSignature: options.requireSignature,
27
+ availableCapabilities: options.availableCapabilities || hostPack.capabilities.required,
28
+ allowUnsafeSandbox: options.allowUnsafeSandbox,
29
+ allowMissingCapabilities: options.allowMissingCapabilities
30
+ });
31
+ }
32
+ return { ...installed, plugins: graph.plugins, pluginGraph: graph };
33
+ }
34
+
35
+ module.exports = { installAppPack };
@@ -0,0 +1,21 @@
1
+ const { createMemoryOperationLog } = require("../memory.cjs");
2
+ const { HostPluginRuntime } = require("../runtime/HostPluginRuntime.cjs");
3
+
4
+ async function runMigrations(store, installed, options = {}) {
5
+ const state = await store.load();
6
+ if (!state) return { ok: true, migrated: [] };
7
+ const runtime = new HostPluginRuntime({
8
+ room: options.room || { id: state.roomId || "room", appPack: installed.roomAppPack },
9
+ store,
10
+ plugins: installed.plugins,
11
+ operationLog: createMemoryOperationLog(),
12
+ authenticateActor: async (_auth, actor) => actor,
13
+ capabilities: options.capabilities || installed.hostPack?.capabilities?.required || []
14
+ });
15
+ const migratedState = await runtime.migrateExistingState(state);
16
+ const changed = JSON.stringify(migratedState) !== JSON.stringify(state);
17
+ if (changed) await store.save(migratedState);
18
+ return { ok: true, migrated: changed ? Object.keys(migratedState.pluginVersions || {}) : [], state: migratedState };
19
+ }
20
+
21
+ module.exports = { runMigrations };
@@ -0,0 +1,25 @@
1
+ const { installHostPack } = require("../plugins/install.cjs");
2
+ const { HostPluginRuntime } = require("../runtime/HostPluginRuntime.cjs");
3
+
4
+ function pluginRegistryFromPlugins(plugins = []) {
5
+ return Object.fromEntries(plugins.flatMap((plugin) => [[plugin.id, plugin], [plugin.source, plugin]].filter((entry) => entry[0])));
6
+ }
7
+
8
+ function createHostPluginRuntimeFromPack(options = {}) {
9
+ const installed = installHostPack({
10
+ appPack: options.appPack,
11
+ hostPack: options.hostPack,
12
+ pluginRegistry: pluginRegistryFromPlugins(options.plugins)
13
+ });
14
+ return new HostPluginRuntime({
15
+ ...options,
16
+ room: {
17
+ ...(options.room || {}),
18
+ id: options.room?.id,
19
+ appPack: installed.roomAppPack
20
+ },
21
+ plugins: installed.plugins
22
+ });
23
+ }
24
+
25
+ module.exports = { createHostPluginRuntimeFromPack, pluginRegistryFromPlugins };
@@ -0,0 +1,95 @@
1
+ const { manifestHash } = require("@mh-gg/base");
2
+ const { runtimeError } = require("../errors.cjs");
3
+ const { createMemoryRoomStore } = require("../memory.cjs");
4
+ const { HostPluginRuntime } = require("../runtime/HostPluginRuntime.cjs");
5
+ const { createRoleKeyAuthenticator, grantArray, operationKeyRoleToActorRole } = require("../security/roleKeys/index.cjs");
6
+ const { installAppPack } = require("./installAppPack.cjs");
7
+ const { runMigrations } = require("./migrations.cjs");
8
+
9
+ function initialMembersFromGrants(grants = []) {
10
+ const members = {};
11
+ for (const grant of grantArray(grants)) {
12
+ if (!grant?.memberId) continue;
13
+ members[grant.memberId] = {
14
+ ...(members[grant.memberId] || {}),
15
+ id: grant.memberId,
16
+ role: operationKeyRoleToActorRole(grant.role),
17
+ credentialId: grant.credentialId,
18
+ deviceId: grant.deviceId
19
+ };
20
+ }
21
+ return members;
22
+ }
23
+
24
+ async function startRoomHost(options = {}) {
25
+ const installed = options.installed || await installAppPack(options.appPackRef, options);
26
+ const roomId = options.room?.id || options.roomName;
27
+ if (!roomId) throw runtimeError("INVALID_ROOM", "roomName or room.id is required");
28
+ const room = {
29
+ ...(options.room || {}),
30
+ id: roomId,
31
+ appPack: installed.roomAppPack,
32
+ hostPack: {
33
+ id: installed.hostPack.id,
34
+ version: installed.hostPack.version,
35
+ hash: manifestHash(installed.hostPack),
36
+ pluginGraphHash: installed.hostPack.compatibility.pluginGraphHash
37
+ }
38
+ };
39
+ const store = options.store || createMemoryRoomStore();
40
+ const operationRoleKeyGrants = options.operationRoleKeyGrants || options.operationKeyGrants || installed.operationRoleKeyGrants;
41
+ const operationGrantAuthorities = options.operationGrantAuthorities || options.grantAuthorities || installed.operationGrantAuthorities;
42
+ const authenticateActor = options.authenticateActor || (operationRoleKeyGrants ? createRoleKeyAuthenticator({
43
+ roomId: room.id,
44
+ appPackId: room.appPack.id,
45
+ appPackHash: room.appPack.hash,
46
+ grants: operationRoleKeyGrants,
47
+ grantAuthorities: operationGrantAuthorities,
48
+ requireSignedGrants: options.requireSignedOperationRoleKeyGrants || options.requireSignedGrants,
49
+ allowUnsignedOperationRoleKeyGrants: options.allowUnsignedOperationRoleKeyGrants,
50
+ devUnsafeUnsignedOperationRoleKeyGrants: options.devUnsafeUnsignedOperationRoleKeyGrants,
51
+ productionRuntime: options.productionRuntime,
52
+ relayExecuted: options.relayExecuted,
53
+ now: options.now
54
+ }) : undefined);
55
+ await runMigrations(store, installed, { ...options, room });
56
+ const runtime = new HostPluginRuntime({
57
+ ...options,
58
+ authenticateActor,
59
+ room,
60
+ store,
61
+ plugins: installed.plugins,
62
+ capabilities: options.capabilities || installed.hostPack.capabilities.required,
63
+ playerPacks: options.playerPacks || [],
64
+ initialMembers: options.initialMembers || initialMembersFromGrants(operationRoleKeyGrants),
65
+ strictStanding: options.strictStanding
66
+ });
67
+ await runtime.start();
68
+ if (options.relay?.registerRoom) {
69
+ try {
70
+ await options.relay.registerRoom({
71
+ room,
72
+ runtime,
73
+ installed,
74
+ appPack: installed.roomAppPack,
75
+ hostPack: installed.hostPack,
76
+ plugins: installed.plugins,
77
+ operationRoleKeyGrants,
78
+ operationGrantAuthorities,
79
+ allowUnsignedOperationRoleKeyGrants: options.allowUnsignedOperationRoleKeyGrants,
80
+ state: await runtime.getState(),
81
+ operations: typeof runtime.operationLog?.list === "function" ? await runtime.operationLog.list() : []
82
+ });
83
+ } catch (error) {
84
+ throw runtimeError("RELAY_REGISTRATION_FAILED", error?.message || "Relay registration failed");
85
+ }
86
+ }
87
+ return {
88
+ room,
89
+ runtime,
90
+ installed,
91
+ inviteUrl: options.createInviteUrl ? options.createInviteUrl({ room, installed }) : `matterhorn://room/${encodeURIComponent(room.id)}?app=${encodeURIComponent(room.appPack.id)}&appHash=${encodeURIComponent(room.appPack.hash)}`
92
+ };
93
+ }
94
+
95
+ module.exports = { startRoomHost };
package/src/index.cjs ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ ...require("./constants.cjs"),
3
+ ...require("./errors.cjs"),
4
+ ...require("./memory.cjs"),
5
+ ...require("./state.cjs"),
6
+ ...require("./plugins/definition.cjs"),
7
+ ...require("./plugins/operationDescriptors.cjs"),
8
+ ...require("./plugins/install.cjs"),
9
+ ...require("./runtime/HostPluginRuntime.cjs"),
10
+ ...require("./host/installAppPack.cjs"),
11
+ ...require("./host/migrations.cjs"),
12
+ ...require("./host/runtimeFromPack.cjs"),
13
+ ...require("./host/startRoomHost.cjs"),
14
+ ...require("./security/roleKeys/index.cjs"),
15
+ ...require("./security/roles.cjs"),
16
+ ...require("./security/scopedRoles.cjs"),
17
+ ...require("@mh-gg/authority"),
18
+ ...require("./security/standingAuthority.cjs"),
19
+ ...require("./security/roomDeviceKeys.cjs"),
20
+ ...require("./security/authorization/coreGate.cjs"),
21
+ ...require("./runtime/directMessages.cjs"),
22
+ ...require("./runtime/readTags.cjs")
23
+ };
package/src/memory.cjs ADDED
@@ -0,0 +1,81 @@
1
+ const { clone } = require("./shared.cjs");
2
+
3
+ function createMemoryRoomStore(initialState = null) {
4
+ let state = clone(initialState);
5
+ return {
6
+ async load() {
7
+ return clone(state);
8
+ },
9
+ async save(nextState) {
10
+ state = clone(nextState);
11
+ }
12
+ };
13
+ }
14
+
15
+ function createMemoryOperationLog(initialEntries = []) {
16
+ const entries = initialEntries.map(clone);
17
+ const operationIds = new Set();
18
+ for (const entry of entries) {
19
+ if (!entry?.id) continue;
20
+ if (operationIds.has(entry.id)) throw new Error(`Duplicate operation id ${entry.id}`);
21
+ operationIds.add(entry.id);
22
+ }
23
+ return {
24
+ entries,
25
+ async append(operation) {
26
+ if (operation?.id && operationIds.has(operation.id)) throw new Error(`Duplicate operation id ${operation.id}`);
27
+ if (operation?.id) operationIds.add(operation.id);
28
+ entries.push(clone(operation));
29
+ },
30
+ async removeById(operationId) {
31
+ const index = entries.findIndex((entry) => entry?.id === operationId);
32
+ if (index < 0) return false;
33
+ const [removed] = entries.splice(index, 1);
34
+ if (removed?.id) operationIds.delete(removed.id);
35
+ return true;
36
+ },
37
+ async list() {
38
+ return entries.map(clone);
39
+ },
40
+ async findById(operationId) {
41
+ const found = entries.find((entry) => entry.id === operationId);
42
+ return found ? clone(found) : null;
43
+ }
44
+ };
45
+ }
46
+
47
+ function createMemoryPluginStorage() {
48
+ const values = new Map();
49
+ return {
50
+ async get(key) {
51
+ return values.has(key) ? clone(values.get(key)) : null;
52
+ },
53
+ async put(key, value) {
54
+ values.set(key, clone(value));
55
+ },
56
+ async delete(key) {
57
+ values.delete(key);
58
+ }
59
+ };
60
+ }
61
+
62
+ function createMemoryPluginEvents() {
63
+ const events = [];
64
+ return {
65
+ async append(event) {
66
+ events.push(clone(event));
67
+ },
68
+ async query(filter = {}) {
69
+ return events
70
+ .filter((event) => Object.entries(filter).every(([key, value]) => value === undefined || event[key] === value))
71
+ .map(clone);
72
+ }
73
+ };
74
+ }
75
+
76
+ module.exports = {
77
+ createMemoryOperationLog,
78
+ createMemoryPluginEvents,
79
+ createMemoryPluginStorage,
80
+ createMemoryRoomStore
81
+ };
@@ -0,0 +1,19 @@
1
+ const { runtimeError } = require("../errors.cjs");
2
+ const { validatePluginOperationDescriptors } = require("./operationDescriptors.cjs");
3
+
4
+ function validatePlugin(plugin) {
5
+ if (!plugin || typeof plugin !== "object") throw runtimeError("INVALID_PLUGIN", "Plugin must be an object");
6
+ if (!plugin.id || !plugin.version) throw runtimeError("INVALID_PLUGIN", "Plugin id and version are required");
7
+ if (typeof plugin.createInitialState !== "function") throw runtimeError("INVALID_PLUGIN", `${plugin.id} createInitialState is required`);
8
+ if (typeof plugin.authorize !== "function") throw runtimeError("INVALID_PLUGIN", `${plugin.id} authorize is required`);
9
+ if (typeof plugin.reduce !== "function") throw runtimeError("INVALID_PLUGIN", `${plugin.id} reduce is required`);
10
+ if (!plugin.schemas?.state || !plugin.schemas?.operations) throw runtimeError("INVALID_PLUGIN", `${plugin.id} schemas are required`);
11
+ validatePluginOperationDescriptors(plugin);
12
+ }
13
+
14
+ function defineHostPlugin(plugin) {
15
+ validatePlugin(plugin);
16
+ return plugin;
17
+ }
18
+
19
+ module.exports = { defineHostPlugin, validatePlugin };
@@ -0,0 +1,90 @@
1
+ const {
2
+ hashCanonical,
3
+ manifestHash,
4
+ validateAppPackManifest,
5
+ validateHostPackManifest
6
+ } = require("@mh-gg/base");
7
+ const { runtimeError } = require("../errors.cjs");
8
+ const { validatePlugin } = require("./definition.cjs");
9
+
10
+ function pluginArtifactIntegrity(plugin) {
11
+ return hashCanonical({ id: plugin.id, version: plugin.version, schema: plugin.schemaDescriptor || null });
12
+ }
13
+
14
+ function clone(value) {
15
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
16
+ }
17
+
18
+ function compositionPluginConfigs(hostPack) {
19
+ const configs = new Map();
20
+ for (const ref of hostPack.composition?.plugins || []) {
21
+ if (ref?.id && ref.config !== undefined) configs.set(ref.id, clone(ref.config));
22
+ }
23
+ return configs;
24
+ }
25
+
26
+ function resolveHostPlugins(hostPack, pluginRegistry) {
27
+ const pack = validateHostPackManifest(hostPack);
28
+ const registry = pluginRegistry || {};
29
+ const resolved = [];
30
+ const byId = new Map(pack.plugins.map((ref) => [ref.id, ref]));
31
+ const configs = compositionPluginConfigs(pack);
32
+
33
+ for (const ref of pack.plugins) {
34
+ for (const dependency of ref.dependsOn || []) {
35
+ const target = byId.get(dependency.id);
36
+ if (!target && !dependency.optional) throw runtimeError("PLUGIN_DEPENDENCY_MISSING", `Plugin ${ref.id} depends on ${dependency.id}`);
37
+ if (target && dependency.version && target.version !== dependency.version) throw runtimeError("PLUGIN_DEPENDENCY_VERSION", `Plugin ${ref.id} depends on ${dependency.id}@${dependency.version}`);
38
+ }
39
+ for (const conflictId of ref.conflictsWith || []) {
40
+ if (byId.has(conflictId)) throw runtimeError("PLUGIN_CONFLICT", `Plugin ${ref.id} conflicts with ${conflictId}`);
41
+ }
42
+ }
43
+
44
+ const visiting = new Set();
45
+ const visited = new Set();
46
+ function visit(ref) {
47
+ if (visited.has(ref.id)) return;
48
+ if (visiting.has(ref.id)) throw runtimeError("PLUGIN_DEPENDENCY_CYCLE", `Plugin dependency cycle at ${ref.id}`);
49
+ visiting.add(ref.id);
50
+ for (const dependency of ref.dependsOn || []) {
51
+ const target = byId.get(dependency.id);
52
+ if (target) visit(target);
53
+ }
54
+ visiting.delete(ref.id);
55
+ visited.add(ref.id);
56
+ const plugin = registry[ref.id] || registry[ref.source];
57
+ if (!plugin) throw runtimeError("PLUGIN_NOT_INSTALLED", `Host plugin ${ref.id} is not available`);
58
+ validatePlugin(plugin);
59
+ if (plugin.id !== ref.id) throw runtimeError("PLUGIN_ID_MISMATCH", `Plugin ${ref.id} resolved to ${plugin.id}`);
60
+ if (plugin.version !== ref.version) throw runtimeError("PLUGIN_VERSION_MISMATCH", `Plugin ${ref.id} version mismatch`);
61
+ if (ref.integrity && plugin.integrity && ref.integrity !== plugin.integrity) throw runtimeError("PLUGIN_INTEGRITY_MISMATCH", `Plugin ${ref.id} integrity mismatch`);
62
+ resolved.push(configs.has(ref.id) ? { ...plugin, config: configs.get(ref.id) } : plugin);
63
+ }
64
+ for (const ref of pack.plugins) visit(ref);
65
+ return resolved;
66
+ }
67
+
68
+ function installHostPack(input) {
69
+ const appPack = validateAppPackManifest(input.appPack);
70
+ const hostPack = validateHostPackManifest(input.hostPack);
71
+ if (hostPack.appPackId !== appPack.id) throw runtimeError("APP_PACK_MISMATCH", "Host pack appPackId does not match app pack");
72
+ if (appPack.hostPack.integrity !== manifestHash(hostPack)) throw runtimeError("HOST_PACK_INTEGRITY_MISMATCH", "Host pack hash does not match app pack pin");
73
+ if (hostPack.compatibility.appProtocolHash !== appPack.compatibility.appProtocolHash) {
74
+ throw runtimeError("APP_PROTOCOL_MISMATCH", "Host pack protocol hash does not match app pack");
75
+ }
76
+ const plugins = resolveHostPlugins(hostPack, input.pluginRegistry);
77
+ return {
78
+ appPack,
79
+ hostPack,
80
+ plugins,
81
+ roomAppPack: {
82
+ id: appPack.id,
83
+ version: appPack.version,
84
+ hash: manifestHash(appPack),
85
+ protocolHash: appPack.compatibility.appProtocolHash
86
+ }
87
+ };
88
+ }
89
+
90
+ module.exports = { installHostPack, pluginArtifactIntegrity, resolveHostPlugins };
@@ -0,0 +1,27 @@
1
+ const { runtimeError, MatterhornRuntimeError } = require("../errors.cjs");
2
+ const { clone } = require("../shared.cjs");
3
+ const { parseWithSchema } = require("../state.cjs");
4
+
5
+ function migrationKey(from, to) {
6
+ return `${from}->${to}`;
7
+ }
8
+
9
+ async function migratePluginState(plugin, roomState, ctx) {
10
+ const currentVersion = roomState.pluginVersions?.[plugin.id];
11
+ if (!currentVersion || currentVersion === plugin.version) {
12
+ return parseWithSchema(plugin.schemas.state, roomState.plugins[plugin.id], `${plugin.id} state`);
13
+ }
14
+ const migrations = plugin.migrations || {};
15
+ let migration = migrations[migrationKey(currentVersion, plugin.version)] || migrations[plugin.version];
16
+ if (!migration && Array.isArray(migrations)) migration = migrations.find((candidate) => candidate.from === currentVersion && candidate.to === plugin.version)?.migrate;
17
+ if (typeof migration !== "function") throw runtimeError("MIGRATION_MISSING", `No migration for ${plugin.id} ${currentVersion} -> ${plugin.version}`);
18
+ try {
19
+ const migrated = await migration(ctx, clone(roomState.plugins[plugin.id]), { from: currentVersion, to: plugin.version });
20
+ return parseWithSchema(plugin.schemas.state, migrated, `${plugin.id} migrated state`);
21
+ } catch (error) {
22
+ if (error instanceof MatterhornRuntimeError) throw error;
23
+ throw runtimeError("MIGRATION_FAILED", `${plugin.id} migration failed: ${error?.message || error}`);
24
+ }
25
+ }
26
+
27
+ module.exports = { migratePluginState, migrationKey };
@@ -0,0 +1,138 @@
1
+ const { runtimeError } = require('../errors.cjs');
2
+ const { normalizeRoomRole } = require('../security/roles.cjs');
3
+ const { clone } = require('../shared.cjs');
4
+
5
+ const OPERATION_SCHEMA_DESCRIPTOR_KIND = 'matterhorn.operation-schema-descriptor';
6
+ const FORBIDDEN_DESCRIPTOR_SHAPES = Object.freeze([
7
+ 'operationPolicyDescriptor',
8
+ 'operationDescriptors',
9
+ 'operationSchemaDescriptors',
10
+ 'operationSchemas'
11
+ ]);
12
+
13
+ function isPlainObject(value) {
14
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+
17
+ function normalizeOperationsObject(operations, label = 'operationSchemaDescriptor.operations') {
18
+ if (!isPlainObject(operations)) {
19
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${label} must be a canonical operation descriptor map`);
20
+ }
21
+ return Object.fromEntries(Object.entries(operations).map(([type, descriptor]) => [type, clone(descriptor)]));
22
+ }
23
+
24
+ function createOperationSchemaDescriptor(input, version, operations) {
25
+ if (isPlainObject(input) && arguments.length === 1) {
26
+ const plugin = input.plugin || input.pluginId;
27
+ if (typeof plugin !== 'string' || plugin.length === 0) throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', 'operation descriptor plugin is required');
28
+ if (typeof input.version !== 'string' || input.version.length === 0) throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', 'operation descriptor version is required');
29
+ return {
30
+ kind: input.kind || OPERATION_SCHEMA_DESCRIPTOR_KIND,
31
+ plugin,
32
+ version: input.version,
33
+ ...(input.source ? { source: input.source } : {}),
34
+ operations: normalizeOperationsObject(input.operations)
35
+ };
36
+ }
37
+ if (typeof input !== 'string' || input.length === 0) throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', 'operation descriptor plugin is required');
38
+ if (typeof version !== 'string' || version.length === 0) throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', 'operation descriptor version is required');
39
+ return {
40
+ kind: OPERATION_SCHEMA_DESCRIPTOR_KIND,
41
+ plugin: input,
42
+ version,
43
+ operations: normalizeOperationsObject(operations)
44
+ };
45
+ }
46
+
47
+ function canonicalOperationDescriptor(plugin) {
48
+ return plugin?.operationSchemaDescriptor;
49
+ }
50
+
51
+ function operationDescriptor(plugin, type) {
52
+ const descriptor = canonicalOperationDescriptor(plugin);
53
+ if (!isPlainObject(descriptor?.operations)) return undefined;
54
+ return descriptor.operations[type];
55
+ }
56
+
57
+ function declaredRolesForOperation(plugin, type) {
58
+ const descriptor = operationDescriptor(plugin, type);
59
+ const roles = descriptor?.authorize?.roles;
60
+ return Array.isArray(roles) ? roles.slice() : undefined;
61
+ }
62
+
63
+ function assertNoLegacyDescriptorShapes(plugin) {
64
+ for (const property of FORBIDDEN_DESCRIPTOR_SHAPES) {
65
+ if (Object.prototype.hasOwnProperty.call(plugin, property)) {
66
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id} must use canonical operationSchemaDescriptor.operations; legacy ${property} is not supported`);
67
+ }
68
+ }
69
+ if (plugin.schemas?.operationsDescriptor) {
70
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id} must use canonical operationSchemaDescriptor.operations; schemas.operationsDescriptor is not supported`);
71
+ }
72
+ for (const [type, schema] of Object.entries(plugin.schemas?.operations || {})) {
73
+ if (schema?.descriptor) {
74
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id}.${type} must declare policy in operationSchemaDescriptor.operations, not schemas.operations.${type}.descriptor`);
75
+ }
76
+ }
77
+ }
78
+
79
+ function assertRoleArray(plugin, type, roles) {
80
+ if (!Array.isArray(roles) || roles.length === 0) {
81
+ throw runtimeError('OPERATION_ROLE_POLICY_MISSING', `${plugin.id}.${type} must declare authorize.roles in operationSchemaDescriptor.operations`);
82
+ }
83
+ for (const role of roles) {
84
+ if (typeof role !== 'string' || role.length === 0 || normalizeRoomRole(role, undefined) === undefined) {
85
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id}.${type} has invalid authorize.roles entry ${JSON.stringify(role)}`);
86
+ }
87
+ }
88
+ }
89
+
90
+ function validatePluginOperationDescriptors(plugin) {
91
+ assertNoLegacyDescriptorShapes(plugin);
92
+ const descriptor = canonicalOperationDescriptor(plugin);
93
+ if (!isPlainObject(descriptor) || !isPlainObject(descriptor.operations)) {
94
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id} must declare canonical operationSchemaDescriptor.operations`);
95
+ }
96
+ if (descriptor.kind !== undefined && descriptor.kind !== OPERATION_SCHEMA_DESCRIPTOR_KIND) {
97
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id} operationSchemaDescriptor.kind is invalid`);
98
+ }
99
+ if (descriptor.plugin !== plugin.id) {
100
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id} operationSchemaDescriptor.plugin must match plugin id`);
101
+ }
102
+ if (descriptor.version !== plugin.version) {
103
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id} operationSchemaDescriptor.version must match plugin version`);
104
+ }
105
+
106
+ const schemaOperationTypes = Object.keys(plugin.schemas?.operations || {}).sort();
107
+ const descriptorOperationTypes = Object.keys(descriptor.operations).sort();
108
+ for (const type of schemaOperationTypes) {
109
+ if (!Object.prototype.hasOwnProperty.call(descriptor.operations, type)) {
110
+ throw runtimeError('OPERATION_ROLE_POLICY_MISSING', `${plugin.id}.${type} must declare authorize.roles in operationSchemaDescriptor.operations`);
111
+ }
112
+ assertRoleArray(plugin, type, descriptor.operations[type]?.authorize?.roles);
113
+ }
114
+ for (const type of descriptorOperationTypes) {
115
+ if (!Object.prototype.hasOwnProperty.call(plugin.schemas.operations, type)) {
116
+ throw runtimeError('INVALID_PLUGIN_OPERATION_DESCRIPTOR', `${plugin.id}.${type} has an operation descriptor but no schemas.operations entry`);
117
+ }
118
+ }
119
+ return true;
120
+ }
121
+
122
+ function requireDeclaredRolePolicy(plugin, type) {
123
+ const roles = declaredRolesForOperation(plugin, type);
124
+ if (!Array.isArray(roles) || roles.length === 0) {
125
+ throw runtimeError('RUNTIME_CONFIGURATION_INVALID', `${plugin.id}.${type} has no validated canonical authorize.roles policy`);
126
+ }
127
+ return roles;
128
+ }
129
+
130
+ module.exports = {
131
+ OPERATION_SCHEMA_DESCRIPTOR_KIND,
132
+ canonicalOperationDescriptor,
133
+ createOperationSchemaDescriptor,
134
+ declaredRolesForOperation,
135
+ operationDescriptor,
136
+ requireDeclaredRolePolicy,
137
+ validatePluginOperationDescriptors
138
+ };