@mh-gg/base 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.
@@ -0,0 +1,174 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+ const { fileURLToPath } = require("node:url");
4
+ const { APP_PACK_KIND, HOST_PACK_KIND, PLAYER_PACK_KIND } = require("../constants.cjs");
5
+ const { fail } = require("../errors.cjs");
6
+ const { assertManifestIntegrity, manifestHash, sha256Bytes } = require("../manifest/hashing.cjs");
7
+ const { isObject, requireObject, requireString, requireHash, validateAppPackManifest, validateHostPackManifest, validatePackRef, validatePlayerPackManifest } = require("../manifest/validators.cjs");
8
+
9
+ function normalizePackRef(ref, name = "packRef") {
10
+ if (typeof ref === "string") return { url: ref };
11
+ const value = requireObject(ref, name);
12
+ const normalized = { url: requireString(value.url, `${name}.url`) };
13
+ if (value.integrity !== undefined) normalized.integrity = requireHash(value.integrity, `${name}.integrity`);
14
+ if (value.kind !== undefined) normalized.kind = requireString(value.kind, `${name}.kind`);
15
+ return normalized;
16
+ }
17
+
18
+ function isHttpUrl(url) {
19
+ return /^https?:\/\//.test(url);
20
+ }
21
+
22
+ function isLocalhost(hostname) {
23
+ return ["localhost", "127.0.0.1", "::1"].includes(String(hostname || "").toLowerCase());
24
+ }
25
+
26
+ function assertRemotePackFetchAllowed(packRef, options = {}) {
27
+ if (!isHttpUrl(packRef.url)) return;
28
+ let parsed;
29
+ try { parsed = new URL(packRef.url); } catch { fail(`Invalid remote pack URL ${packRef.url}`); }
30
+ if (parsed.protocol === "http:" && !isLocalhost(parsed.hostname) && options.allowInsecureHttp !== true) {
31
+ fail("Insecure HTTP pack references are disabled by default. Use HTTPS or pass allowInsecureHttp for trusted local development.");
32
+ }
33
+ const hasIntegrity = Boolean(packRef.integrity || options.expectedIntegrity);
34
+ if (!hasIntegrity && options.allowRemoteWithoutIntegrity !== true) {
35
+ fail("Remote pack artifact integrity is required by default.");
36
+ }
37
+ }
38
+
39
+ function pathFromFileUrlOrPath(url) {
40
+ if (url.startsWith("file:")) return fileURLToPath(url);
41
+ return path.resolve(url);
42
+ }
43
+
44
+ function isPathInside(child, parent) {
45
+ const relative = path.relative(parent, child);
46
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
47
+ }
48
+
49
+ async function loadPackArtifact(ref, options = {}) {
50
+ const packRef = normalizePackRef(ref);
51
+ let bytes;
52
+ let source = packRef.url;
53
+
54
+ if (packRef.url.startsWith("workspace:")) {
55
+ if (typeof options.workspaceResolver !== "function") fail(`No workspace resolver for ${packRef.url}`);
56
+ const resolved = await options.workspaceResolver(packRef.url);
57
+ if (Buffer.isBuffer(resolved)) bytes = resolved;
58
+ else if (typeof resolved === "string") bytes = Buffer.from(resolved);
59
+ else bytes = Buffer.from(JSON.stringify(resolved));
60
+ } else if (isHttpUrl(packRef.url)) {
61
+ assertRemotePackFetchAllowed(packRef, options);
62
+ if (typeof options.fetch !== "function") fail(`No fetch implementation for ${packRef.url}`);
63
+ const response = await options.fetch(packRef.url);
64
+ if (!response || (response.ok === false)) fail(`Unable to fetch ${packRef.url}`);
65
+ const arrayBuffer = typeof response.arrayBuffer === "function" ? await response.arrayBuffer() : Buffer.from(await response.text());
66
+ bytes = Buffer.from(arrayBuffer);
67
+ } else {
68
+ bytes = await fs.readFile(pathFromFileUrlOrPath(packRef.url));
69
+ }
70
+
71
+ const contentHash = sha256Bytes(bytes);
72
+ if (packRef.integrity && packRef.integrity !== contentHash) {
73
+ return { ok: false, code: "INTEGRITY_MISMATCH", url: source, expected: packRef.integrity, actual: contentHash, bytes };
74
+ }
75
+ return { ok: true, url: source, contentHash, bytes };
76
+ }
77
+
78
+ function validatorForKind(kind) {
79
+ if (kind === APP_PACK_KIND) return validateAppPackManifest;
80
+ if (kind === HOST_PACK_KIND) return validateHostPackManifest;
81
+ if (kind === PLAYER_PACK_KIND) return validatePlayerPackManifest;
82
+ fail(`Unsupported manifest kind ${kind}`);
83
+ }
84
+
85
+ async function loadPackManifest(ref, options = {}) {
86
+ const packRef = normalizePackRef(ref);
87
+ const artifact = await loadPackArtifact({ ...packRef, integrity: undefined }, { ...options, expectedIntegrity: packRef.integrity || options.expectedIntegrity });
88
+ if (!artifact.ok) return artifact;
89
+ let manifest;
90
+ try {
91
+ manifest = JSON.parse(artifact.bytes.toString("utf8"));
92
+ } catch (error) {
93
+ return { ok: false, code: "MALFORMED_JSON", url: artifact.url, error: error.message };
94
+ }
95
+ if (!manifest || typeof manifest.kind !== "string") return { ok: false, code: "MANIFEST_KIND_MISSING", url: artifact.url };
96
+ if (packRef.kind && manifest.kind !== packRef.kind) return { ok: false, code: "MANIFEST_KIND_MISMATCH", url: artifact.url, expected: packRef.kind, actual: manifest.kind };
97
+ try {
98
+ validatorForKind(manifest.kind)(manifest);
99
+ } catch (error) {
100
+ return { ok: false, code: "MANIFEST_INVALID", url: artifact.url, error: error.message };
101
+ }
102
+ const contentHash = artifact.contentHash;
103
+ const hash = manifestHash(manifest);
104
+ if (packRef.integrity && packRef.integrity !== contentHash && packRef.integrity !== hash) {
105
+ return { ok: false, code: "INTEGRITY_MISMATCH", url: artifact.url, expected: packRef.integrity, actual: contentHash, manifestHash: hash };
106
+ }
107
+ return { ok: true, url: artifact.url, contentHash, manifestHash: hash, manifest };
108
+ }
109
+
110
+ function parsePackReferenceUrl(url) {
111
+ const text = requireString(url, "url");
112
+ const hashIndex = text.indexOf("#");
113
+ const base = hashIndex === -1 ? text : text.slice(0, hashIndex);
114
+ const exportName = hashIndex === -1 ? undefined : text.slice(hashIndex + 1);
115
+
116
+ if (text.startsWith("workspace:")) {
117
+ const body = base.slice("workspace:".length);
118
+ if (!body) fail("workspace pack reference is missing a package name");
119
+ return { scheme: "workspace", packageName: body, exportName };
120
+ }
121
+ if (text.startsWith("file:")) {
122
+ const filePath = base.slice("file:".length);
123
+ if (!filePath) fail("file pack reference is missing a path");
124
+ return { scheme: "file", path: filePath, exportName };
125
+ }
126
+ if (text.startsWith("http://") || text.startsWith("https://")) return { scheme: "http", url: base, exportName };
127
+ fail(`Unsupported pack reference URL ${text}`);
128
+ }
129
+
130
+ async function readJsonFile(filePath, options = {}) {
131
+ const baseDir = options.baseDir || process.cwd();
132
+ const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(baseDir, filePath);
133
+ if (options.baseDir && options.allowFileOutsideBase !== true && !isPathInside(resolved, path.resolve(baseDir))) {
134
+ fail("File pack reference escapes the configured base directory");
135
+ }
136
+ return JSON.parse(await fs.readFile(resolved, "utf8"));
137
+ }
138
+
139
+ function resolveWorkspaceExport(registry, parsed, originalUrl) {
140
+ if (registry?.[originalUrl] !== undefined) return registry[originalUrl];
141
+ const packageEntry = registry?.[parsed.packageName];
142
+ if (packageEntry === undefined) fail(`Workspace pack ${parsed.packageName} is not registered`);
143
+ if (!parsed.exportName) return packageEntry;
144
+ if (packageEntry[parsed.exportName] === undefined) fail(`Workspace pack ${parsed.packageName} does not export ${parsed.exportName}`);
145
+ return packageEntry[parsed.exportName];
146
+ }
147
+
148
+ async function loadPackReference(ref, options = {}) {
149
+ const packRef = validatePackRef(ref, "pack reference");
150
+ const parsed = parsePackReferenceUrl(packRef.url);
151
+ let manifest;
152
+ if (parsed.scheme === "workspace") manifest = resolveWorkspaceExport(options.workspace || options.registry, parsed, packRef.url);
153
+ else if (parsed.scheme === "file") {
154
+ manifest = await readJsonFile(parsed.path, options);
155
+ if (parsed.exportName) manifest = manifest[parsed.exportName];
156
+ } else if (parsed.scheme === "http") {
157
+ assertRemotePackFetchAllowed({ url: parsed.url, integrity: packRef.integrity }, options);
158
+ if (typeof options.fetchJson !== "function") fail("fetchJson is required for HTTP pack references");
159
+ manifest = await options.fetchJson(parsed.url);
160
+ if (parsed.exportName) manifest = manifest[parsed.exportName];
161
+ }
162
+ if (!isObject(manifest)) fail("Pack reference did not resolve to a manifest object");
163
+ assertManifestIntegrity(manifest, packRef.integrity);
164
+ return manifest;
165
+ }
166
+
167
+ module.exports = {
168
+ assertRemotePackFetchAllowed,
169
+ loadPackArtifact,
170
+ loadPackManifest,
171
+ loadPackReference,
172
+ normalizePackRef,
173
+ parsePackReferenceUrl
174
+ };
@@ -0,0 +1,28 @@
1
+ const { fail } = require("../errors.cjs");
2
+ const { manifestHash } = require("../manifest/hashing.cjs");
3
+ const { requireString } = require("../manifest/validators.cjs");
4
+
5
+ function createInMemoryPackRegistry(initial = {}) {
6
+ const manifests = new Map();
7
+ for (const [url, manifest] of Object.entries(initial)) manifests.set(url, JSON.parse(JSON.stringify(manifest)));
8
+ return {
9
+ publish(url, manifest) {
10
+ requireString(url, "url");
11
+ manifests.set(url, JSON.parse(JSON.stringify(manifest)));
12
+ return { url, integrity: manifestHash(manifest) };
13
+ },
14
+ async fetchJson(url) {
15
+ if (!manifests.has(url)) fail(`Manifest ${url} not found`);
16
+ return JSON.parse(JSON.stringify(manifests.get(url)));
17
+ },
18
+ ref(url) {
19
+ if (!manifests.has(url)) fail(`Manifest ${url} not found`);
20
+ return { url, integrity: manifestHash(manifests.get(url)) };
21
+ },
22
+ list() {
23
+ return [...manifests.keys()];
24
+ }
25
+ };
26
+ }
27
+
28
+ module.exports = { createInMemoryPackRegistry };
@@ -0,0 +1,80 @@
1
+ const { fail } = require("../errors.cjs");
2
+ const { requireArray, requireObject, requireString, validateAppPackManifest, validatePlayerPackManifest, validatePlayerSupport } = require("../manifest/validators.cjs");
3
+
4
+ function definePlayerPlugin(plugin) {
5
+ const value = requireObject(plugin, "player plugin");
6
+ requireString(value.id, "player plugin.id");
7
+ requireString(value.version, "player plugin.version");
8
+ if (value.supports !== undefined) requireArray(value.supports, "player plugin.supports").forEach((support, index) => validatePlayerSupport(support, `player plugin.supports[${index}]`));
9
+ return value;
10
+ }
11
+
12
+ function parseSemver(value, name) {
13
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(requireString(value, name));
14
+ if (!match) fail(`${name} must be a x.y.z version`);
15
+ return match.slice(1).map((part) => Number(part));
16
+ }
17
+
18
+ function compareSemver(left, right) {
19
+ for (let index = 0; index < 3; index += 1) {
20
+ if (left[index] !== right[index]) return left[index] - right[index];
21
+ }
22
+ return 0;
23
+ }
24
+
25
+ function satisfiesVersion(version, range) {
26
+ if (range === "*") return true;
27
+ if (range.startsWith("^")) {
28
+ const actual = parseSemver(version, "version");
29
+ const base = parseSemver(range.slice(1), "range");
30
+ return actual[0] === base[0] && compareSemver(actual, base) >= 0;
31
+ }
32
+ return version === range;
33
+ }
34
+
35
+ function playerSupportsApp(playerPack, appPack) {
36
+ const player = validatePlayerPackManifest(playerPack);
37
+ const app = validateAppPackManifest(appPack);
38
+ return player.supports.some((support) =>
39
+ support.appPackId === app.id &&
40
+ support.appProtocolHash === app.compatibility.appProtocolHash &&
41
+ satisfiesVersion(app.version, support.appPackRange)
42
+ );
43
+ }
44
+
45
+ function includes(value, candidate) {
46
+ return Array.isArray(value) && value.includes(candidate);
47
+ }
48
+
49
+ function scorePlayerPack(player, input) {
50
+ let score = 0;
51
+ const roomId = input.room?.id;
52
+ if (roomId && input.userPreferences?.defaultByRoom?.[roomId] === player.id) score += 1000;
53
+ if (input.userPreferences?.defaultByAppPack?.[input.appPack.id] === player.id) score += 500;
54
+ if (input.groupPreferences?.recommendedFrontends?.[input.appPack.id] === player.id) score += 250;
55
+ if (includes(player.recommendedFor?.devices, input.device?.class)) score += 100;
56
+ if (input.actor && includes(player.recommendedFor?.roles, input.actor.role)) score += 100;
57
+ if (input.device?.lowBandwidth && includes(player.recommendedFor?.devices, "low-bandwidth")) score += 50;
58
+ return score;
59
+ }
60
+
61
+ function choosePlayerPacks(input) {
62
+ const value = requireObject(input, "input");
63
+ const appPack = validateAppPackManifest(value.appPack);
64
+ const playerPacks = requireArray(value.playerPacks, "playerPacks");
65
+ const isTrustedPublisher = value.isTrustedPublisher || (() => true);
66
+
67
+ return playerPacks
68
+ .filter((player) => playerSupportsApp(player, appPack))
69
+ .filter((player) => isTrustedPublisher(player.publisher))
70
+ .map((player) => ({ player, score: scorePlayerPack(player, { ...value, appPack }) }))
71
+ .sort((left, right) => right.score - left.score || left.player.id.localeCompare(right.player.id))
72
+ .map((entry) => entry.player);
73
+ }
74
+
75
+ module.exports = {
76
+ choosePlayerPacks,
77
+ definePlayerPlugin,
78
+ playerSupportsApp,
79
+ satisfiesVersion
80
+ };
@@ -0,0 +1,194 @@
1
+ const { fail } = require("../errors.cjs");
2
+ const { createCapabilityPrompt, evaluateCapabilityGrant } = require("../capabilities.cjs");
3
+ const { hashCanonical, manifestHash } = require("../manifest/hashing.cjs");
4
+ const { satisfiesVersion } = require("../players/index.cjs");
5
+ const { requireArray, requireHash, requireObject, requireString, validateAppPackManifest, validateHostPackManifest } = require("../manifest/validators.cjs");
6
+
7
+ function normalizePluginDependency(dep, name) {
8
+ const value = requireObject(dep, name);
9
+ return {
10
+ id: requireString(value.id, `${name}.id`),
11
+ range: value.range === undefined ? "*" : requireString(value.range, `${name}.range`),
12
+ optional: Boolean(value.optional)
13
+ };
14
+ }
15
+
16
+ function pluginIdentity(plugin) {
17
+ const value = requireObject(plugin, "plugin");
18
+ return {
19
+ id: requireString(value.id, "plugin.id"),
20
+ version: requireString(value.version, "plugin.version"),
21
+ stateSchemaHash: value.stateSchemaHash || hashCanonical(value.schemas?.state?.descriptor || value.schemas?.state || {}),
22
+ operationSchemaHash: value.operationSchemaHash || hashCanonical(value.operationSchemaDescriptor || {}),
23
+ eventSchemaHash: value.eventSchemaHash
24
+ };
25
+ }
26
+
27
+ function pluginDependencies(plugin) {
28
+ const raw = plugin.dependencies || {};
29
+ const required = (raw.required || []).map((dep, index) => normalizePluginDependency(dep, `${plugin.id}.dependencies.required[${index}]`));
30
+ const optional = (raw.optional || []).map((dep, index) => ({ ...normalizePluginDependency(dep, `${plugin.id}.dependencies.optional[${index}]`), optional: true }));
31
+ const conflicts = (plugin.conflicts || []).map((dep, index) => normalizePluginDependency(dep, `${plugin.id}.conflicts[${index}]`));
32
+ return { required, optional, conflicts };
33
+ }
34
+
35
+ function resolvePluginGraph(plugins) {
36
+ const input = requireArray(plugins, "plugins", { nonEmpty: true });
37
+ const byId = new Map();
38
+ for (const plugin of input) {
39
+ const id = requireString(plugin.id, "plugin.id");
40
+ if (byId.has(id)) fail(`Duplicate plugin ${id}`);
41
+ byId.set(id, plugin);
42
+ }
43
+
44
+ for (const plugin of byId.values()) {
45
+ const deps = pluginDependencies(plugin);
46
+ for (const dep of deps.required) {
47
+ const target = byId.get(dep.id);
48
+ if (!target) fail(`Plugin ${plugin.id} requires missing dependency ${dep.id}`);
49
+ if (!satisfiesVersion(target.version, dep.range)) fail(`Plugin ${plugin.id} requires ${dep.id} ${dep.range}, got ${target.version}`);
50
+ }
51
+ for (const conflict of deps.conflicts) {
52
+ const target = byId.get(conflict.id);
53
+ if (target && satisfiesVersion(target.version, conflict.range)) fail(`Plugin ${plugin.id} conflicts with ${conflict.id} ${target.version}`);
54
+ }
55
+ }
56
+
57
+ const ordered = [];
58
+ const visiting = new Set();
59
+ const visited = new Set();
60
+ function visit(plugin, pathStack = []) {
61
+ if (visited.has(plugin.id)) return;
62
+ if (visiting.has(plugin.id)) fail(`Plugin dependency cycle: ${[...pathStack, plugin.id].join(" -> ")}`);
63
+ visiting.add(plugin.id);
64
+ const deps = pluginDependencies(plugin);
65
+ for (const dep of [...deps.required, ...deps.optional].sort((a, b) => a.id.localeCompare(b.id))) {
66
+ const target = byId.get(dep.id);
67
+ if (target) visit(target, [...pathStack, plugin.id]);
68
+ }
69
+ visiting.delete(plugin.id);
70
+ visited.add(plugin.id);
71
+ ordered.push(plugin);
72
+ }
73
+ for (const plugin of [...byId.values()].sort((a, b) => a.id.localeCompare(b.id))) visit(plugin);
74
+
75
+ const descriptors = ordered.map(pluginIdentity);
76
+ return { plugins: ordered, descriptors, pluginGraphHash: hashCanonical(descriptors) };
77
+ }
78
+
79
+ function normalizeHostPluginDescriptor(value, name = "hostPlugins[]") {
80
+ const plugin = requireObject(value, name);
81
+ return {
82
+ id: requireString(plugin.id, `${name}.id`),
83
+ version: requireString(plugin.version, `${name}.version`),
84
+ stateSchemaHash: requireHash(plugin.stateSchemaHash, `${name}.stateSchemaHash`),
85
+ operationSchemaHash: requireHash(plugin.operationSchemaHash, `${name}.operationSchemaHash`),
86
+ ...(plugin.eventSchemaHash === undefined ? {} : { eventSchemaHash: requireHash(plugin.eventSchemaHash, `${name}.eventSchemaHash`) })
87
+ };
88
+ }
89
+
90
+ function normalizeHostPluginDescriptors(hostPlugins) {
91
+ return requireArray(hostPlugins, "hostPlugins", { nonEmpty: true })
92
+ .map((plugin, index) => normalizeHostPluginDescriptor(plugin, `hostPlugins[${index}]`))
93
+ .sort((left, right) => left.id.localeCompare(right.id) || left.version.localeCompare(right.version));
94
+ }
95
+
96
+ function computeAppProtocolHash(input) {
97
+ const value = requireObject(input, "input");
98
+ const appPackId = requireString(value.appPackId, "appPackId");
99
+ const appVersion = requireString(value.appVersion, "appVersion");
100
+ const plugins = normalizeHostPluginDescriptors(value.hostPlugins);
101
+ return hashCanonical({ appPackId, appVersion, plugins });
102
+ }
103
+
104
+ function createMatterhornAppContract(input) {
105
+ const value = requireObject(input, "input");
106
+ const appPack = validateAppPackManifest(value.appPack);
107
+ const hostPlugins = normalizeHostPluginDescriptors(value.hostPlugins);
108
+ const appProtocolHash = computeAppProtocolHash({ appPackId: appPack.id, appVersion: appPack.version, hostPlugins });
109
+ if (appPack.compatibility.appProtocolHash !== appProtocolHash) {
110
+ fail(`App protocol hash mismatch: expected ${appPack.compatibility.appProtocolHash}, got ${appProtocolHash}`);
111
+ }
112
+ return { appPackId: appPack.id, appPackVersion: appPack.version, appPackHash: manifestHash(appPack), hostPlugins, appProtocolHash };
113
+ }
114
+
115
+ function normalizePluginRef(ref) {
116
+ return {
117
+ id: ref.id,
118
+ version: ref.version,
119
+ source: ref.source,
120
+ integrity: ref.integrity,
121
+ dependsOn: (ref.dependsOn || []).map((dependency) => ({
122
+ id: dependency.id,
123
+ ...(dependency.version === undefined ? {} : { version: dependency.version }),
124
+ ...(dependency.optional === undefined ? {} : { optional: dependency.optional })
125
+ })),
126
+ conflictsWith: [...(ref.conflictsWith || [])],
127
+ capabilities: ref.capabilities ? {
128
+ required: [...(ref.capabilities.required || [])],
129
+ optional: [...(ref.capabilities.optional || [])]
130
+ } : undefined
131
+ };
132
+ }
133
+
134
+ function resolveHostPluginGraph(hostPack, pluginRegistry = {}) {
135
+ const pack = validateHostPackManifest(hostPack);
136
+ const refs = pack.plugins.map((plugin, index) => normalizePluginRef(require("../manifest/validators.cjs").validateHostPluginRef(plugin, `plugins[${index}]`)));
137
+ const byId = new Map();
138
+
139
+ for (const ref of refs) {
140
+ if (byId.has(ref.id)) fail(`Duplicate host plugin ${ref.id}`);
141
+ byId.set(ref.id, ref);
142
+ }
143
+
144
+ for (const ref of refs) {
145
+ for (const conflictId of ref.conflictsWith || []) {
146
+ if (byId.has(conflictId)) fail(`Host plugin ${ref.id} conflicts with ${conflictId}`);
147
+ }
148
+ for (const dependency of ref.dependsOn || []) {
149
+ const target = byId.get(dependency.id);
150
+ if (!target) {
151
+ if (dependency.optional) continue;
152
+ fail(`Host plugin ${ref.id} depends on missing plugin ${dependency.id}`);
153
+ }
154
+ if (dependency.version !== undefined && target.version !== dependency.version) {
155
+ fail(`Host plugin ${ref.id} depends on ${dependency.id}@${dependency.version}, got ${target.version}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ const ordered = [];
161
+ const visiting = new Set();
162
+ const visited = new Set();
163
+ function visit(ref) {
164
+ if (visited.has(ref.id)) return;
165
+ if (visiting.has(ref.id)) fail(`Host plugin dependency cycle at ${ref.id}`);
166
+ visiting.add(ref.id);
167
+ for (const dependency of ref.dependsOn || []) {
168
+ const target = byId.get(dependency.id);
169
+ if (target) visit(target);
170
+ }
171
+ visiting.delete(ref.id);
172
+ visited.add(ref.id);
173
+ const plugin = pluginRegistry[ref.id] || pluginRegistry[ref.source];
174
+ ordered.push({ ref, plugin });
175
+ }
176
+ for (const ref of refs) visit(ref);
177
+
178
+ return {
179
+ appPackId: pack.appPackId,
180
+ hostPackId: pack.id,
181
+ pluginGraphHash: pack.compatibility.pluginGraphHash,
182
+ refs: ordered.map((entry) => entry.ref),
183
+ plugins: ordered.map((entry) => entry.plugin).filter(Boolean),
184
+ missingImplementations: ordered.filter((entry) => !entry.plugin).map((entry) => entry.ref.id)
185
+ };
186
+ }
187
+
188
+ module.exports = {
189
+ computeAppProtocolHash,
190
+ createMatterhornAppContract,
191
+ pluginIdentity,
192
+ resolveHostPluginGraph,
193
+ resolvePluginGraph
194
+ };
@@ -0,0 +1,124 @@
1
+ const { PUBLISHER_TRUST } = require("../constants.cjs");
2
+ const { fail } = require("../errors.cjs");
3
+ const { manifestHash, verifySignedManifest } = require("../manifest/hashing.cjs");
4
+ const { validateHostPackManifest, validatePublisherRef, requireString } = require("../manifest/validators.cjs");
5
+ const { capabilityAudit } = require("../capabilities.cjs");
6
+
7
+ function createMemoryTrustStore(initial = []) {
8
+ const records = new Map();
9
+ for (const item of initial) {
10
+ const key = `${item.scope || "user"}:${item.publisherId}:${item.publicKey || "*"}`;
11
+ records.set(key, { scope: "user", trusted: true, blocked: false, ...item });
12
+ }
13
+ return {
14
+ trustPublisher(record) {
15
+ const item = { scope: "user", trusted: true, blocked: false, createdAt: Date.now(), ...record };
16
+ if (!item.publisherId && item.id) item.publisherId = item.id;
17
+ requireString(item.publisherId, "publisherId");
18
+ const key = `${item.scope}:${item.publisherId}:${item.publicKey || "*"}`;
19
+ records.set(key, item);
20
+ return item;
21
+ },
22
+ blockPublisher(record) {
23
+ return this.trustPublisher({ ...record, trusted: false, blocked: true });
24
+ },
25
+ isTrusted(publisher, options = {}) {
26
+ if (!publisher) return false;
27
+ const scopes = [options.scope, "group", "user"].filter(Boolean);
28
+ const candidates = [];
29
+ for (const scope of scopes) {
30
+ candidates.push(`${scope}:${publisher.id}:${publisher.publicKey}`);
31
+ candidates.push(`${scope}:${publisher.id}:*`);
32
+ }
33
+ const values = candidates.map((key) => records.get(key)).filter(Boolean);
34
+ if (values.some((record) => record.blocked)) return false;
35
+ return values.some((record) => record.trusted);
36
+ },
37
+ export() {
38
+ return [...records.values()].map((record) => JSON.parse(JSON.stringify(record)));
39
+ }
40
+ };
41
+ }
42
+
43
+ function createPublisherTrustStore(initial = {}) {
44
+ const publishers = new Map();
45
+ for (const [publisherId, record] of Object.entries(initial.publishers || initial)) {
46
+ const status = typeof record === "string" ? record : record.status;
47
+ if (!PUBLISHER_TRUST.has(status)) fail(`Invalid trust status ${status}`);
48
+ publishers.set(publisherId, { publisherId, status, ...(typeof record === "object" ? record : {}) });
49
+ }
50
+ return {
51
+ trust(publisherId, details = {}) {
52
+ publishers.set(requireString(publisherId, "publisherId"), { publisherId, status: "trusted", ...details });
53
+ },
54
+ block(publisherId, details = {}) {
55
+ publishers.set(requireString(publisherId, "publisherId"), { publisherId, status: "blocked", ...details });
56
+ },
57
+ review(publisherId, details = {}) {
58
+ publishers.set(requireString(publisherId, "publisherId"), { publisherId, status: "review", ...details });
59
+ },
60
+ status(publisher) {
61
+ const publisherId = typeof publisher === "string" ? publisher : publisher?.id;
62
+ return publishers.get(publisherId)?.status || "review";
63
+ },
64
+ isTrusted(publisher) {
65
+ return this.status(publisher) === "trusted";
66
+ },
67
+ isBlocked(publisher) {
68
+ return this.status(publisher) === "blocked";
69
+ },
70
+ snapshot() {
71
+ return { publishers: Object.fromEntries(publishers.entries()) };
72
+ }
73
+ };
74
+ }
75
+
76
+ function publisherTrustStatus(trustStore, publisher, options = {}) {
77
+ if (!trustStore) return options.requireTrusted === false ? "unverified" : "review";
78
+ if (typeof trustStore.isBlocked === "function" && trustStore.isBlocked(publisher, options)) return "blocked";
79
+ if (typeof trustStore.status === "function") return trustStore.status(publisher);
80
+ if (typeof trustStore.isTrusted === "function") return trustStore.isTrusted(publisher, options) ? "trusted" : "review";
81
+ return "review";
82
+ }
83
+
84
+ function publisherMayUseSignatureKey(trustStore, publisher, publicKey, options = {}) {
85
+ if (publicKey === publisher.publicKey) return true;
86
+ if (typeof trustStore?.status === "function") return false;
87
+ if (typeof trustStore?.isTrusted === "function") return trustStore.isTrusted({ ...publisher, publicKey }, options);
88
+ return false;
89
+ }
90
+
91
+ function assertTrustedManifest(manifest, trustStore, options = {}) {
92
+ const publisher = validatePublisherRef(manifest.publisher || { id: manifest.id, name: manifest.id, publicKey: "unknown" });
93
+ const status = publisherTrustStatus(trustStore, publisher, options);
94
+ if (status === "blocked") fail(`Publisher ${publisher.id} is blocked`);
95
+ if (status !== "trusted" && options.requireTrusted !== false) fail(`Publisher ${publisher.id} is not trusted`);
96
+ if (options.requireSignature !== false) {
97
+ const verified = verifySignedManifest(manifest, {
98
+ isTrustedPublicKey: (publicKey) => publisherMayUseSignatureKey(trustStore, publisher, publicKey, options)
99
+ });
100
+ if (!verified.ok) fail(`Manifest ${manifest.id || manifest.kind} does not have a trusted signature`);
101
+ }
102
+ return { ok: true, publisher, status, hash: manifestHash(manifest) };
103
+ }
104
+
105
+ function assertProductionPluginGates(input = {}) {
106
+ const hostPack = validateHostPackManifest(input.hostPack);
107
+ const sandbox = hostPack.runtime?.sandbox || "none";
108
+ const production = input.production !== false;
109
+ const trustStore = input.trustStore;
110
+ if (production && !trustStore && input.allowUntrustedLocal !== true) fail("Production host pack trust store is required");
111
+ if (trustStore && input.requireSignature !== false && !input.publisher) fail("Production host pack publisher is required for signature verification");
112
+ const trustResult = trustStore ? assertTrustedManifest({ ...hostPack, publisher: input.publisher || { id: hostPack.id, name: hostPack.id, publicKey: "unknown" } }, trustStore, { requireSignature: input.requireSignature }) : { ok: true };
113
+ if (production && sandbox === "none" && input.allowUnsafeSandbox !== true) fail("Production mode refuses host packs without a sandbox");
114
+ const audit = capabilityAudit({ capabilities: hostPack.capabilities, available: input.availableCapabilities || [] });
115
+ if (!audit.ok && input.allowMissingCapabilities !== true) fail(`Missing required capabilities: ${audit.missingRequired.join(", ")}`);
116
+ return { ok: true, sandbox, trust: trustResult, capabilityAudit: audit };
117
+ }
118
+
119
+ module.exports = {
120
+ assertProductionPluginGates,
121
+ assertTrustedManifest,
122
+ createMemoryTrustStore,
123
+ createPublisherTrustStore
124
+ };