@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.
- package/package.json +20 -0
- package/src/capabilities.cjs +113 -0
- package/src/constants.cjs +15 -0
- package/src/errors.cjs +12 -0
- package/src/index.cjs +13 -0
- package/src/manifest/factory.cjs +72 -0
- package/src/manifest/hashing.cjs +73 -0
- package/src/manifest/validators.cjs +203 -0
- package/src/packs/artifacts.cjs +174 -0
- package/src/packs/registry.cjs +28 -0
- package/src/players/index.cjs +80 -0
- package/src/plugins/graph.cjs +194 -0
- package/src/trust/store.cjs +124 -0
- package/test/matterhorn-core.test.cjs +731 -0
- package/test/pack-security.test.cjs +51 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mh-gg/base",
|
|
3
|
+
"version": "0.1.1-alpha.20260613T085325975Z",
|
|
4
|
+
"description": "Matterhorn v0.1 app, host, player pack manifest and contract helpers.",
|
|
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
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.12"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.test.cjs",
|
|
18
|
+
"coverage": "node --test --experimental-test-coverage --test-coverage-lines=80 --test-coverage-functions=80 --test-coverage-branches=80 --test-coverage-include=src/**/*.cjs test/*.test.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const { fail } = require("./errors.cjs");
|
|
2
|
+
const { requireObject, requireString, validateCapabilities } = require("./manifest/validators.cjs");
|
|
3
|
+
|
|
4
|
+
function normalizeCapabilities(...sets) {
|
|
5
|
+
const required = new Set();
|
|
6
|
+
const optional = new Set();
|
|
7
|
+
for (const set of sets) {
|
|
8
|
+
if (!set) continue;
|
|
9
|
+
const caps = set.capabilities || set;
|
|
10
|
+
for (const capability of caps.required || caps.requires || []) required.add(requireString(capability, "capability"));
|
|
11
|
+
for (const capability of caps.optional || caps.provides || []) optional.add(requireString(capability, "capability"));
|
|
12
|
+
}
|
|
13
|
+
for (const capability of required) optional.delete(capability);
|
|
14
|
+
return { required: [...required].sort(), optional: [...optional].sort() };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const HIGH_RISK_CAPABILITIES = new Set(["network.fetch", "storage.attachments", "media.sfu", "ai.invoke"]);
|
|
18
|
+
|
|
19
|
+
function createCapabilityPrompt(input) {
|
|
20
|
+
const value = requireObject(input, "input");
|
|
21
|
+
const capabilities = normalizeCapabilities(...(value.sources || []));
|
|
22
|
+
const granted = new Set(value.granted || []);
|
|
23
|
+
const denied = new Set(value.denied || []);
|
|
24
|
+
return {
|
|
25
|
+
publisher: value.publisher,
|
|
26
|
+
required: capabilities.required.map((capability) => ({
|
|
27
|
+
capability,
|
|
28
|
+
granted: granted.has(capability),
|
|
29
|
+
denied: denied.has(capability),
|
|
30
|
+
highRisk: HIGH_RISK_CAPABILITIES.has(capability)
|
|
31
|
+
})),
|
|
32
|
+
optional: capabilities.optional.map((capability) => ({
|
|
33
|
+
capability,
|
|
34
|
+
granted: granted.has(capability),
|
|
35
|
+
denied: denied.has(capability),
|
|
36
|
+
highRisk: HIGH_RISK_CAPABILITIES.has(capability)
|
|
37
|
+
}))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function evaluateCapabilityGrant(prompt) {
|
|
42
|
+
const requiredDenied = (prompt.required || []).filter((item) => item.denied || !item.granted);
|
|
43
|
+
return {
|
|
44
|
+
ok: requiredDenied.length === 0,
|
|
45
|
+
deniedRequired: requiredDenied.map((item) => item.capability),
|
|
46
|
+
granted: [...(prompt.required || []), ...(prompt.optional || [])].filter((item) => item.granted && !item.denied).map((item) => item.capability).sort(),
|
|
47
|
+
declinedOptional: (prompt.optional || []).filter((item) => item.denied || !item.granted).map((item) => item.capability).sort()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatCapabilityAudit(prompt) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
if (prompt.publisher) lines.push(`Publisher: ${prompt.publisher.name || prompt.publisher.id}`);
|
|
54
|
+
for (const item of prompt.required || []) lines.push(`required ${item.highRisk ? "high-risk " : ""}${item.capability}: ${item.granted && !item.denied ? "granted" : "missing"}`);
|
|
55
|
+
for (const item of prompt.optional || []) lines.push(`optional ${item.highRisk ? "high-risk " : ""}${item.capability}: ${item.granted && !item.denied ? "granted" : "declined"}`);
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeCapabilityInput(input) {
|
|
60
|
+
if (Array.isArray(input)) return { required: input, optional: [] };
|
|
61
|
+
const value = validateCapabilities(input || { required: [] });
|
|
62
|
+
return { required: value.required || [], optional: value.optional || [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function capabilityAudit(input = {}) {
|
|
66
|
+
const requiredOptional = normalizeCapabilityInput(input.capabilities || input);
|
|
67
|
+
const available = new Set(input.available || []);
|
|
68
|
+
const required = [...requiredOptional.required];
|
|
69
|
+
const optional = [...(requiredOptional.optional || [])];
|
|
70
|
+
const missingRequired = required.filter((capability) => !available.has(capability));
|
|
71
|
+
const missingOptional = optional.filter((capability) => !available.has(capability));
|
|
72
|
+
return {
|
|
73
|
+
ok: missingRequired.length === 0,
|
|
74
|
+
required,
|
|
75
|
+
optional,
|
|
76
|
+
available: [...available],
|
|
77
|
+
missingRequired,
|
|
78
|
+
missingOptional,
|
|
79
|
+
promptLines: [
|
|
80
|
+
...(required.length ? [`Required: ${required.join(", ")}`] : []),
|
|
81
|
+
...(optional.length ? [`Optional: ${optional.join(", ")}`] : []),
|
|
82
|
+
...(missingRequired.length ? [`Missing required: ${missingRequired.join(", ")}`] : [])
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function enforceProductionPluginPolicy(input) {
|
|
88
|
+
const value = requireObject(input, "input");
|
|
89
|
+
const production = value.production === true;
|
|
90
|
+
if (!production) return { ok: true, mode: "development" };
|
|
91
|
+
const publisherTrusted = value.publisherTrusted !== false;
|
|
92
|
+
if (!publisherTrusted) fail("Production mode rejects untrusted publishers");
|
|
93
|
+
const { validateHostPackManifest } = require("./manifest/validators.cjs");
|
|
94
|
+
const hostPack = value.hostPack ? validateHostPackManifest(value.hostPack) : undefined;
|
|
95
|
+
if (hostPack?.runtime?.sandbox === "none" && value.remote === true) fail("Production mode rejects remote host plugins without a sandbox");
|
|
96
|
+
const prompt = value.capabilityPrompt ? value.capabilityPrompt : createCapabilityPrompt({ sources: [hostPack || {}], granted: value.grantedCapabilities || [] });
|
|
97
|
+
const grant = evaluateCapabilityGrant(prompt);
|
|
98
|
+
if (!grant.ok) fail(`Production mode requires capability grants: ${grant.deniedRequired.join(", ")}`);
|
|
99
|
+
for (const plugin of value.plugins || []) {
|
|
100
|
+
if (plugin.reduce?.constructor?.name === "AsyncFunction") fail(`Plugin ${plugin.id} has an async reducer, which is not deterministic`);
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, mode: "production", granted: grant.granted };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
HIGH_RISK_CAPABILITIES,
|
|
107
|
+
capabilityAudit,
|
|
108
|
+
createCapabilityPrompt,
|
|
109
|
+
enforceProductionPluginPolicy,
|
|
110
|
+
evaluateCapabilityGrant,
|
|
111
|
+
formatCapabilityAudit,
|
|
112
|
+
normalizeCapabilities
|
|
113
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const APP_PACK_KIND = "matterhorn.app-pack";
|
|
2
|
+
const HOST_PACK_KIND = "matterhorn.host-pack";
|
|
3
|
+
const PLAYER_PACK_KIND = "matterhorn.player-pack";
|
|
4
|
+
const PLAYER_MODES = new Set(["external", "embedded", "native", "launcher-plugin"]);
|
|
5
|
+
const HOST_SANDBOXES = new Set(["none", "process", "worker", "wasm"]);
|
|
6
|
+
const PUBLISHER_TRUST = new Set(["trusted", "blocked", "review"]);
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
APP_PACK_KIND,
|
|
10
|
+
HOST_PACK_KIND,
|
|
11
|
+
HOST_SANDBOXES,
|
|
12
|
+
PLAYER_MODES,
|
|
13
|
+
PLAYER_PACK_KIND,
|
|
14
|
+
PUBLISHER_TRUST
|
|
15
|
+
};
|
package/src/errors.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class MatterhornManifestError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "MatterhornManifestError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function fail(message) {
|
|
9
|
+
throw new MatterhornManifestError(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = { MatterhornManifestError, fail };
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
...require("./constants.cjs"),
|
|
3
|
+
...require("./errors.cjs"),
|
|
4
|
+
...require("./capabilities.cjs"),
|
|
5
|
+
...require("./manifest/validators.cjs"),
|
|
6
|
+
...require("./manifest/hashing.cjs"),
|
|
7
|
+
...require("./manifest/factory.cjs"),
|
|
8
|
+
...require("./packs/artifacts.cjs"),
|
|
9
|
+
...require("./packs/registry.cjs"),
|
|
10
|
+
...require("./players/index.cjs"),
|
|
11
|
+
...require("./plugins/graph.cjs"),
|
|
12
|
+
...require("./trust/store.cjs")
|
|
13
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { fail } = require("../errors.cjs");
|
|
2
|
+
const { assertManifestIntegrity, manifestHash } = require("./hashing.cjs");
|
|
3
|
+
const { playerSupportsApp } = require("../players/index.cjs");
|
|
4
|
+
const { requireArray, requireObject, validateAppPackManifest, validateHostPackManifest, validatePlayerPackManifest } = require("./validators.cjs");
|
|
5
|
+
|
|
6
|
+
function createAppPackManifest(input) {
|
|
7
|
+
return JSON.parse(JSON.stringify(validateAppPackManifest(input)));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createHostPackManifest(input) {
|
|
11
|
+
return JSON.parse(JSON.stringify(validateHostPackManifest(input)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createPlayerPackManifest(input) {
|
|
15
|
+
return JSON.parse(JSON.stringify(validatePlayerPackManifest(input)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createPackBundle(input) {
|
|
19
|
+
const value = requireObject(input, "input");
|
|
20
|
+
const appPack = validateAppPackManifest(value.appPack);
|
|
21
|
+
const hostPack = validateHostPackManifest(value.hostPack);
|
|
22
|
+
const playerPacks = requireArray(value.playerPacks, "playerPacks", { nonEmpty: true }).map((player, index) => validatePlayerPackManifest(player, `playerPacks[${index}]`));
|
|
23
|
+
|
|
24
|
+
if (hostPack.appPackId !== appPack.id) fail("Host pack appPackId does not match app pack");
|
|
25
|
+
assertManifestIntegrity(hostPack, appPack.hostPack.integrity);
|
|
26
|
+
|
|
27
|
+
const refs = new Map(appPack.playerPacks.map((playerRef) => [playerRef.id, playerRef]));
|
|
28
|
+
for (const playerPack of playerPacks) {
|
|
29
|
+
const ref = refs.get(playerPack.id);
|
|
30
|
+
if (!ref) fail(`Player pack ${playerPack.id} is not referenced by app pack`);
|
|
31
|
+
assertManifestIntegrity(playerPack, ref.integrity);
|
|
32
|
+
if (!playerSupportsApp(playerPack, appPack)) fail(`Player pack ${playerPack.id} does not support app pack ${appPack.id}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (hostPack.compatibility.appProtocolHash !== appPack.compatibility.appProtocolHash) {
|
|
36
|
+
fail("Host pack appProtocolHash does not match app pack");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
appPack: JSON.parse(JSON.stringify(appPack)),
|
|
41
|
+
hostPack: JSON.parse(JSON.stringify(hostPack)),
|
|
42
|
+
playerPacks: playerPacks.map((playerPack) => JSON.parse(JSON.stringify(playerPack))),
|
|
43
|
+
appPackHash: manifestHash(appPack),
|
|
44
|
+
hostPackHash: manifestHash(hostPack),
|
|
45
|
+
playerPackHashes: Object.fromEntries(playerPacks.map((playerPack) => [playerPack.id, manifestHash(playerPack)])),
|
|
46
|
+
contract: {
|
|
47
|
+
appPackId: appPack.id,
|
|
48
|
+
appPackVersion: appPack.version,
|
|
49
|
+
appPackHash: manifestHash(appPack),
|
|
50
|
+
appProtocolHash: appPack.compatibility.appProtocolHash,
|
|
51
|
+
hostPackId: hostPack.id,
|
|
52
|
+
hostPackHash: manifestHash(hostPack),
|
|
53
|
+
playerPackIds: playerPacks.map((playerPack) => playerPack.id)
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function verifyPackBundle(input) {
|
|
59
|
+
try {
|
|
60
|
+
return { ok: true, bundle: createPackBundle(input) };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { ok: false, error: error?.message || String(error) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
createAppPackManifest,
|
|
68
|
+
createHostPackManifest,
|
|
69
|
+
createPackBundle,
|
|
70
|
+
createPlayerPackManifest,
|
|
71
|
+
verifyPackBundle
|
|
72
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const { canonicalJson, sha256Hex } = require("@mh-gg/event");
|
|
3
|
+
const { fail } = require("../errors.cjs");
|
|
4
|
+
const { isObject, requireHash, requireObject, requireString, validateSignatures } = require("./validators.cjs");
|
|
5
|
+
|
|
6
|
+
function sha256Bytes(bytes) {
|
|
7
|
+
return `sha256-${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hashCanonical(value) {
|
|
11
|
+
return `sha256-${sha256Hex(canonicalJson(value))}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stripManifestSignatures(manifest) {
|
|
15
|
+
const copy = JSON.parse(JSON.stringify(manifest));
|
|
16
|
+
if (isObject(copy.trust)) delete copy.trust.signatures;
|
|
17
|
+
return copy;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function manifestHash(manifest) {
|
|
21
|
+
return hashCanonical(stripManifestSignatures(manifest));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertManifestIntegrity(manifest, expectedHash) {
|
|
25
|
+
const expected = requireHash(expectedHash, "expectedHash");
|
|
26
|
+
const hash = manifestHash(manifest);
|
|
27
|
+
if (hash !== expected) fail(`Manifest integrity mismatch: expected ${expected}, got ${hash}`);
|
|
28
|
+
return { ok: true, hash };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function signManifest(manifest, privateKey, publicKey) {
|
|
32
|
+
const signature = crypto.sign(null, Buffer.from(manifestHash(manifest)), privateKey).toString("base64url");
|
|
33
|
+
const signed = JSON.parse(JSON.stringify(manifest));
|
|
34
|
+
signed.trust = {
|
|
35
|
+
...(isObject(signed.trust) ? signed.trust : {}),
|
|
36
|
+
signatures: [{ publicKey: requireString(publicKey, "publicKey"), signature }]
|
|
37
|
+
};
|
|
38
|
+
return signed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function signaturePublicKeyAllowed(publicKey, options = {}) {
|
|
42
|
+
if (Array.isArray(options.allowedPublicKeys) && !options.allowedPublicKeys.includes(publicKey)) return false;
|
|
43
|
+
if (typeof options.isTrustedPublicKey === "function" && !options.isTrustedPublicKey(publicKey)) return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function verifySignedManifest(manifest, options = {}) {
|
|
48
|
+
const trust = requireObject(manifest.trust, "trust");
|
|
49
|
+
const signatures = validateSignatures(trust.signatures);
|
|
50
|
+
const hash = manifestHash(manifest);
|
|
51
|
+
|
|
52
|
+
for (const signature of signatures) {
|
|
53
|
+
if (!signaturePublicKeyAllowed(signature.publicKey, options)) continue;
|
|
54
|
+
try {
|
|
55
|
+
if (crypto.verify(null, Buffer.from(hash), signature.publicKey, Buffer.from(signature.signature, "base64url"))) {
|
|
56
|
+
return { ok: true, hash, publicKey: signature.publicKey };
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Bad key material is treated the same as a bad signature.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { ok: false, hash };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
assertManifestIntegrity,
|
|
67
|
+
hashCanonical,
|
|
68
|
+
manifestHash,
|
|
69
|
+
sha256Bytes,
|
|
70
|
+
signManifest,
|
|
71
|
+
stripManifestSignatures,
|
|
72
|
+
verifySignedManifest
|
|
73
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const { APP_PACK_KIND, HOST_PACK_KIND, HOST_SANDBOXES, PLAYER_MODES, PLAYER_PACK_KIND } = require("../constants.cjs");
|
|
2
|
+
const { fail } = require("../errors.cjs");
|
|
3
|
+
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function requireObject(value, name) {
|
|
9
|
+
if (!isObject(value)) fail(`${name} must be an object`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireString(value, name) {
|
|
14
|
+
if (typeof value !== "string" || value.trim() === "") fail(`${name} must be a non-empty string`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireHash(value, name) {
|
|
19
|
+
const text = requireString(value, name);
|
|
20
|
+
if (!text.startsWith("sha256-") || text.length <= "sha256-".length) fail(`${name} must be a sha256 hash`);
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requireArray(value, name, options = {}) {
|
|
25
|
+
if (!Array.isArray(value)) fail(`${name} must be an array`);
|
|
26
|
+
if (options.nonEmpty && value.length === 0) fail(`${name} must not be empty`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireStringArray(value, name) {
|
|
31
|
+
return requireArray(value, name).map((item, index) => requireString(item, `${name}[${index}]`));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireKind(manifest, kind) {
|
|
35
|
+
if (manifest.kind !== kind) fail(`Expected kind ${kind}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validatePublisherRef(value, name = "publisher") {
|
|
39
|
+
const publisher = requireObject(value, name);
|
|
40
|
+
requireString(publisher.id, `${name}.id`);
|
|
41
|
+
requireString(publisher.name, `${name}.name`);
|
|
42
|
+
requireString(publisher.publicKey, `${name}.publicKey`);
|
|
43
|
+
return publisher;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateSignatures(value, name = "trust.signatures") {
|
|
47
|
+
return requireArray(value, name, { nonEmpty: true }).map((signature, index) => {
|
|
48
|
+
const item = requireObject(signature, `${name}[${index}]`);
|
|
49
|
+
requireString(item.publicKey, `${name}[${index}].publicKey`);
|
|
50
|
+
requireString(item.signature, `${name}[${index}].signature`);
|
|
51
|
+
return item;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateTrust(value, options = {}) {
|
|
56
|
+
const trust = requireObject(value, "trust");
|
|
57
|
+
if (options.createdAt) requireString(trust.createdAt, "trust.createdAt");
|
|
58
|
+
validateSignatures(trust.signatures);
|
|
59
|
+
return trust;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateCapabilities(value, name = "capabilities") {
|
|
63
|
+
const capabilities = requireObject(value, name);
|
|
64
|
+
requireStringArray(capabilities.required, `${name}.required`);
|
|
65
|
+
if (capabilities.optional !== undefined) requireStringArray(capabilities.optional, `${name}.optional`);
|
|
66
|
+
return capabilities;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validatePackRef(value, name) {
|
|
70
|
+
const ref = requireObject(value, name);
|
|
71
|
+
requireString(ref.url, `${name}.url`);
|
|
72
|
+
requireHash(ref.integrity, `${name}.integrity`);
|
|
73
|
+
return ref;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateRecommendedFor(value, name) {
|
|
77
|
+
if (value === undefined) return undefined;
|
|
78
|
+
const recommended = requireObject(value, name);
|
|
79
|
+
if (recommended.devices !== undefined) requireStringArray(recommended.devices, `${name}.devices`);
|
|
80
|
+
if (recommended.roles !== undefined) requireStringArray(recommended.roles, `${name}.roles`);
|
|
81
|
+
if (recommended.groups !== undefined) requireStringArray(recommended.groups, `${name}.groups`);
|
|
82
|
+
return recommended;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateAppPlayerRef(value, name) {
|
|
86
|
+
const ref = validatePackRef(value, name);
|
|
87
|
+
requireString(ref.id, `${name}.id`);
|
|
88
|
+
requireString(ref.name, `${name}.name`);
|
|
89
|
+
validateRecommendedFor(ref.recommendedFor, `${name}.recommendedFor`);
|
|
90
|
+
return ref;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateCompatibility(value, name, requiredHashes) {
|
|
94
|
+
const compatibility = requireObject(value, name);
|
|
95
|
+
for (const field of requiredHashes) requireHash(compatibility[field], `${name}.${field}`);
|
|
96
|
+
return compatibility;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateAppPackManifest(value) {
|
|
100
|
+
const manifest = requireObject(value, "app pack");
|
|
101
|
+
requireKind(manifest, APP_PACK_KIND);
|
|
102
|
+
requireString(manifest.id, "id");
|
|
103
|
+
requireString(manifest.name, "name");
|
|
104
|
+
requireString(manifest.version, "version");
|
|
105
|
+
validatePublisherRef(manifest.publisher);
|
|
106
|
+
requireString(manifest.matterhornVersion, "matterhornVersion");
|
|
107
|
+
validatePackRef(manifest.hostPack, "hostPack");
|
|
108
|
+
requireArray(manifest.playerPacks, "playerPacks", { nonEmpty: true }).forEach((player, index) => {
|
|
109
|
+
validateAppPlayerRef(player, `playerPacks[${index}]`);
|
|
110
|
+
});
|
|
111
|
+
validateCompatibility(manifest.compatibility, "compatibility", ["appProtocolHash", "operationSchemaHash", "stateSchemaHash"]);
|
|
112
|
+
if (manifest.compatibility.eventSchemaHash !== undefined) requireHash(manifest.compatibility.eventSchemaHash, "compatibility.eventSchemaHash");
|
|
113
|
+
validateCapabilities(manifest.capabilities);
|
|
114
|
+
validateTrust(manifest.trust, { createdAt: true });
|
|
115
|
+
return manifest;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateHostPluginRef(value, name) {
|
|
119
|
+
const ref = requireObject(value, name);
|
|
120
|
+
requireString(ref.id, `${name}.id`);
|
|
121
|
+
requireString(ref.version, `${name}.version`);
|
|
122
|
+
requireString(ref.source, `${name}.source`);
|
|
123
|
+
requireHash(ref.integrity, `${name}.integrity`);
|
|
124
|
+
if (ref.dependsOn !== undefined) {
|
|
125
|
+
requireArray(ref.dependsOn, `${name}.dependsOn`).forEach((dependency, index) => {
|
|
126
|
+
const item = requireObject(dependency, `${name}.dependsOn[${index}]`);
|
|
127
|
+
requireString(item.id, `${name}.dependsOn[${index}].id`);
|
|
128
|
+
if (item.version !== undefined) requireString(item.version, `${name}.dependsOn[${index}].version`);
|
|
129
|
+
if (item.optional !== undefined && typeof item.optional !== "boolean") fail(`${name}.dependsOn[${index}].optional must be a boolean`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (ref.conflictsWith !== undefined) requireStringArray(ref.conflictsWith, `${name}.conflictsWith`);
|
|
133
|
+
if (ref.capabilities !== undefined) validateCapabilities(ref.capabilities, `${name}.capabilities`);
|
|
134
|
+
return ref;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateHostPackManifest(value) {
|
|
138
|
+
const manifest = requireObject(value, "host pack");
|
|
139
|
+
requireKind(manifest, HOST_PACK_KIND);
|
|
140
|
+
requireString(manifest.id, "id");
|
|
141
|
+
requireString(manifest.appPackId, "appPackId");
|
|
142
|
+
requireString(manifest.version, "version");
|
|
143
|
+
requireArray(manifest.plugins, "plugins", { nonEmpty: true }).forEach((plugin, index) => {
|
|
144
|
+
validateHostPluginRef(plugin, `plugins[${index}]`);
|
|
145
|
+
});
|
|
146
|
+
validateCompatibility(manifest.compatibility, "compatibility", ["appProtocolHash", "pluginGraphHash"]);
|
|
147
|
+
const runtime = requireObject(manifest.runtime, "runtime");
|
|
148
|
+
requireString(runtime.minMatterhornVersion, "runtime.minMatterhornVersion");
|
|
149
|
+
if (runtime.sandbox !== undefined && !HOST_SANDBOXES.has(runtime.sandbox)) fail("runtime.sandbox is invalid");
|
|
150
|
+
validateCapabilities(manifest.capabilities);
|
|
151
|
+
validateTrust(manifest.trust);
|
|
152
|
+
return manifest;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validatePlayerSupport(value, name) {
|
|
156
|
+
const support = requireObject(value, name);
|
|
157
|
+
requireString(support.appPackId, `${name}.appPackId`);
|
|
158
|
+
requireString(support.appPackRange, `${name}.appPackRange`);
|
|
159
|
+
requireHash(support.appProtocolHash, `${name}.appProtocolHash`);
|
|
160
|
+
if (support.pluginIds !== undefined) requireStringArray(support.pluginIds, `${name}.pluginIds`);
|
|
161
|
+
return support;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validatePlayerPackManifest(value) {
|
|
165
|
+
const manifest = requireObject(value, "player pack");
|
|
166
|
+
requireKind(manifest, PLAYER_PACK_KIND);
|
|
167
|
+
requireString(manifest.id, "id");
|
|
168
|
+
requireString(manifest.name, "name");
|
|
169
|
+
requireString(manifest.version, "version");
|
|
170
|
+
validatePublisherRef(manifest.publisher);
|
|
171
|
+
const entrypoints = requireObject(manifest.entrypoints, "entrypoints");
|
|
172
|
+
requireString(entrypoints.default, "entrypoints.default");
|
|
173
|
+
for (const field of ["admin", "mobile", "kiosk", "embedded"]) {
|
|
174
|
+
if (entrypoints[field] !== undefined) requireString(entrypoints[field], `entrypoints.${field}`);
|
|
175
|
+
}
|
|
176
|
+
requireArray(manifest.supports, "supports", { nonEmpty: true }).forEach((support, index) => {
|
|
177
|
+
validatePlayerSupport(support, `supports[${index}]`);
|
|
178
|
+
});
|
|
179
|
+
validateRecommendedFor(manifest.recommendedFor, "recommendedFor");
|
|
180
|
+
if (!PLAYER_MODES.has(manifest.mode)) fail("mode is invalid");
|
|
181
|
+
const trust = validateTrust(manifest.trust);
|
|
182
|
+
if (trust.integrity !== undefined) requireHash(trust.integrity, "trust.integrity");
|
|
183
|
+
return manifest;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
isObject,
|
|
188
|
+
requireArray,
|
|
189
|
+
requireHash,
|
|
190
|
+
requireObject,
|
|
191
|
+
requireString,
|
|
192
|
+
requireStringArray,
|
|
193
|
+
validateAppPackManifest,
|
|
194
|
+
validateCapabilities,
|
|
195
|
+
validateHostPackManifest,
|
|
196
|
+
validateHostPluginRef,
|
|
197
|
+
validatePackRef,
|
|
198
|
+
validatePlayerPackManifest,
|
|
199
|
+
validatePlayerSupport,
|
|
200
|
+
validatePublisherRef,
|
|
201
|
+
validateSignatures,
|
|
202
|
+
validateTrust
|
|
203
|
+
};
|