@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
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const crypto = require("node:crypto");
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
APP_PACK_KIND,
|
|
7
|
+
HOST_PACK_KIND,
|
|
8
|
+
PLAYER_PACK_KIND,
|
|
9
|
+
assertManifestIntegrity,
|
|
10
|
+
assertProductionPluginGates,
|
|
11
|
+
assertTrustedManifest,
|
|
12
|
+
capabilityAudit,
|
|
13
|
+
createInMemoryPackRegistry,
|
|
14
|
+
createMemoryTrustStore,
|
|
15
|
+
createPublisherTrustStore,
|
|
16
|
+
choosePlayerPacks,
|
|
17
|
+
computeAppProtocolHash,
|
|
18
|
+
createAppPackManifest,
|
|
19
|
+
createHostPackManifest,
|
|
20
|
+
createPackBundle,
|
|
21
|
+
createPlayerPackManifest,
|
|
22
|
+
createMatterhornAppContract,
|
|
23
|
+
definePlayerPlugin,
|
|
24
|
+
hashCanonical,
|
|
25
|
+
loadPackReference,
|
|
26
|
+
manifestHash,
|
|
27
|
+
parsePackReferenceUrl,
|
|
28
|
+
playerSupportsApp,
|
|
29
|
+
resolveHostPluginGraph,
|
|
30
|
+
satisfiesVersion,
|
|
31
|
+
signManifest,
|
|
32
|
+
stripManifestSignatures,
|
|
33
|
+
validateAppPackManifest,
|
|
34
|
+
validateHostPackManifest,
|
|
35
|
+
validatePlayerPackManifest,
|
|
36
|
+
verifyPackBundle,
|
|
37
|
+
verifySignedManifest
|
|
38
|
+
} = require("../src/index.cjs");
|
|
39
|
+
|
|
40
|
+
const publisher = {
|
|
41
|
+
id: "com.matterhorn",
|
|
42
|
+
name: "Matterhorn Official",
|
|
43
|
+
publicKey: "rk_pub_test"
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const signatures = [
|
|
47
|
+
{
|
|
48
|
+
publicKey: "rk_pub_test",
|
|
49
|
+
signature: "sig_test"
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const hostPlugins = [
|
|
54
|
+
{
|
|
55
|
+
id: "@mh-gg/plugin-kanban",
|
|
56
|
+
version: "1.0.0",
|
|
57
|
+
stateSchemaHash: "sha256-kanban-state",
|
|
58
|
+
operationSchemaHash: "sha256-kanban-ops"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "@mh-gg/plugin-roles",
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
stateSchemaHash: "sha256-roles-state",
|
|
64
|
+
operationSchemaHash: "sha256-roles-ops",
|
|
65
|
+
eventSchemaHash: "sha256-roles-events"
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function appProtocolHash() {
|
|
70
|
+
return computeAppProtocolHash({
|
|
71
|
+
appPackId: "com.matterhorn.kanban",
|
|
72
|
+
appVersion: "1.0.0",
|
|
73
|
+
hostPlugins
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function appPack(overrides = {}) {
|
|
78
|
+
return {
|
|
79
|
+
kind: APP_PACK_KIND,
|
|
80
|
+
id: "com.matterhorn.kanban",
|
|
81
|
+
name: "Kanban",
|
|
82
|
+
version: "1.0.0",
|
|
83
|
+
publisher,
|
|
84
|
+
matterhornVersion: ">=0.1 <0.2",
|
|
85
|
+
hostPack: {
|
|
86
|
+
url: "https://registry.matterhorn.gg/kanban/1.0.0/host-pack.json",
|
|
87
|
+
integrity: "sha256-hostpack"
|
|
88
|
+
},
|
|
89
|
+
playerPacks: [
|
|
90
|
+
{
|
|
91
|
+
id: "com.matterhorn.kanban.desktop",
|
|
92
|
+
name: "Kanban Desktop",
|
|
93
|
+
url: "https://apps.matterhorn.gg/kanban/desktop/1.0.0/player-pack.json",
|
|
94
|
+
integrity: "sha256-player",
|
|
95
|
+
recommendedFor: {
|
|
96
|
+
devices: ["desktop"],
|
|
97
|
+
roles: ["admin", "member"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
compatibility: {
|
|
102
|
+
appProtocolHash: appProtocolHash(),
|
|
103
|
+
operationSchemaHash: "sha256-ops",
|
|
104
|
+
stateSchemaHash: "sha256-state"
|
|
105
|
+
},
|
|
106
|
+
capabilities: {
|
|
107
|
+
required: ["room.state", "room.roles"]
|
|
108
|
+
},
|
|
109
|
+
trust: {
|
|
110
|
+
createdAt: "2026-05-26T00:00:00Z",
|
|
111
|
+
signatures
|
|
112
|
+
},
|
|
113
|
+
...overrides
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hostPack(overrides = {}) {
|
|
118
|
+
return {
|
|
119
|
+
kind: HOST_PACK_KIND,
|
|
120
|
+
id: "com.matterhorn.kanban.host",
|
|
121
|
+
appPackId: "com.matterhorn.kanban",
|
|
122
|
+
version: "1.0.0",
|
|
123
|
+
plugins: [
|
|
124
|
+
{
|
|
125
|
+
id: "@mh-gg/plugin-kanban",
|
|
126
|
+
version: "1.0.0",
|
|
127
|
+
source: "npm:@mh-gg/plugin-kanban@1.0.0",
|
|
128
|
+
integrity: "sha256-kanban"
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
compatibility: {
|
|
132
|
+
appProtocolHash: appProtocolHash(),
|
|
133
|
+
pluginGraphHash: "sha256-graph"
|
|
134
|
+
},
|
|
135
|
+
runtime: {
|
|
136
|
+
minMatterhornVersion: "0.1.0",
|
|
137
|
+
sandbox: "process"
|
|
138
|
+
},
|
|
139
|
+
capabilities: {
|
|
140
|
+
required: ["room.state", "room.roles"]
|
|
141
|
+
},
|
|
142
|
+
trust: {
|
|
143
|
+
signatures
|
|
144
|
+
},
|
|
145
|
+
...overrides
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function playerPack(overrides = {}) {
|
|
150
|
+
return {
|
|
151
|
+
kind: PLAYER_PACK_KIND,
|
|
152
|
+
id: "com.matterhorn.kanban.desktop",
|
|
153
|
+
name: "Kanban Desktop",
|
|
154
|
+
version: "1.0.0",
|
|
155
|
+
publisher,
|
|
156
|
+
entrypoints: {
|
|
157
|
+
default: "https://apps.matterhorn.gg/kanban/desktop/1.0.0/"
|
|
158
|
+
},
|
|
159
|
+
supports: [
|
|
160
|
+
{
|
|
161
|
+
appPackId: "com.matterhorn.kanban",
|
|
162
|
+
appPackRange: "^1.0.0",
|
|
163
|
+
appProtocolHash: appProtocolHash()
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
recommendedFor: {
|
|
167
|
+
devices: ["desktop"],
|
|
168
|
+
roles: ["admin", "member"]
|
|
169
|
+
},
|
|
170
|
+
mode: "external",
|
|
171
|
+
trust: {
|
|
172
|
+
integrity: "sha256-player",
|
|
173
|
+
signatures
|
|
174
|
+
},
|
|
175
|
+
...overrides
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test("manifest hash ignores signatures but preserves signed content", () => {
|
|
180
|
+
const first = appPack();
|
|
181
|
+
const second = appPack({
|
|
182
|
+
trust: {
|
|
183
|
+
createdAt: "2026-05-26T00:00:00Z",
|
|
184
|
+
signatures: [{ publicKey: "rk_pub_other", signature: "sig_other" }]
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
const changed = appPack({ name: "Kanban Changed" });
|
|
188
|
+
|
|
189
|
+
assert.equal(manifestHash(first), manifestHash(second));
|
|
190
|
+
assert.notEqual(manifestHash(first), manifestHash(changed));
|
|
191
|
+
assert.deepEqual(stripManifestSignatures(first).trust, {
|
|
192
|
+
createdAt: "2026-05-26T00:00:00Z"
|
|
193
|
+
});
|
|
194
|
+
assert.match(hashCanonical({ b: 2, a: "x" }), /^sha256-/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("validates app, host, and player pack manifests", () => {
|
|
198
|
+
assert.equal(validateAppPackManifest(appPack()).id, "com.matterhorn.kanban");
|
|
199
|
+
assert.equal(validateHostPackManifest(hostPack()).id, "com.matterhorn.kanban.host");
|
|
200
|
+
assert.equal(validatePlayerPackManifest(playerPack()).id, "com.matterhorn.kanban.desktop");
|
|
201
|
+
|
|
202
|
+
assert.throws(() => validateAppPackManifest(appPack({ kind: "wrong" })), /Expected kind matterhorn\.app-pack/);
|
|
203
|
+
assert.throws(() => validateHostPackManifest(hostPack({ plugins: [] })), /plugins must not be empty/);
|
|
204
|
+
assert.throws(() => validatePlayerPackManifest(playerPack({ mode: "iframe" })), /mode is invalid/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("computes deterministic protocol contracts from plugin schema hashes", () => {
|
|
208
|
+
const protocolA = computeAppProtocolHash({
|
|
209
|
+
appPackId: "com.matterhorn.kanban",
|
|
210
|
+
appVersion: "1.0.0",
|
|
211
|
+
hostPlugins
|
|
212
|
+
});
|
|
213
|
+
const protocolB = computeAppProtocolHash({
|
|
214
|
+
appPackId: "com.matterhorn.kanban",
|
|
215
|
+
appVersion: "1.0.0",
|
|
216
|
+
hostPlugins: [...hostPlugins].reverse()
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
assert.equal(protocolA, protocolB);
|
|
220
|
+
|
|
221
|
+
const contract = createMatterhornAppContract({
|
|
222
|
+
appPack: appPack(),
|
|
223
|
+
hostPlugins
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
assert.deepEqual(contract.hostPlugins.map((plugin) => plugin.id), [
|
|
227
|
+
"@mh-gg/plugin-kanban",
|
|
228
|
+
"@mh-gg/plugin-roles"
|
|
229
|
+
]);
|
|
230
|
+
assert.equal(contract.appPackId, "com.matterhorn.kanban");
|
|
231
|
+
assert.equal(contract.appProtocolHash, protocolA);
|
|
232
|
+
|
|
233
|
+
assert.throws(
|
|
234
|
+
() => createMatterhornAppContract({
|
|
235
|
+
appPack: appPack({ compatibility: { ...appPack().compatibility, appProtocolHash: "sha256-wrong" } }),
|
|
236
|
+
hostPlugins
|
|
237
|
+
}),
|
|
238
|
+
/App protocol hash mismatch/
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("checks manifest integrity using content-addressed hashes", () => {
|
|
243
|
+
const manifest = appPack();
|
|
244
|
+
const expected = manifestHash(manifest);
|
|
245
|
+
|
|
246
|
+
assert.equal(assertManifestIntegrity(manifest, expected).hash, expected);
|
|
247
|
+
assert.throws(() => assertManifestIntegrity(manifest, "sha256-wrong"), /Manifest integrity mismatch/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("signs and verifies manifests against their content hash", () => {
|
|
251
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
|
|
252
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
253
|
+
const trustedPublisher = { ...publisher, publicKey: publicKeyPem };
|
|
254
|
+
const signed = signManifest(
|
|
255
|
+
appPack({ publisher: trustedPublisher }),
|
|
256
|
+
privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
257
|
+
publicKeyPem
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
assert.equal(signed.trust.signatures[0].publicKey, publicKeyPem);
|
|
261
|
+
assert.equal(verifySignedManifest(signed).ok, true);
|
|
262
|
+
assert.equal(assertTrustedManifest(signed, createPublisherTrustStore({ publishers: { [publisher.id]: "trusted" } })).ok, true);
|
|
263
|
+
|
|
264
|
+
const memoryTrust = createMemoryTrustStore();
|
|
265
|
+
assert.throws(() => assertTrustedManifest(signed, memoryTrust), /not trusted/);
|
|
266
|
+
memoryTrust.trustPublisher({ publisherId: publisher.id, publicKey: publicKeyPem });
|
|
267
|
+
assert.equal(assertTrustedManifest(signed, memoryTrust).ok, true);
|
|
268
|
+
|
|
269
|
+
const attacker = crypto.generateKeyPairSync("ed25519");
|
|
270
|
+
const attackerPublicKeyPem = attacker.publicKey.export({ type: "spki", format: "pem" });
|
|
271
|
+
const forged = signManifest(
|
|
272
|
+
appPack({ publisher: trustedPublisher }),
|
|
273
|
+
attacker.privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
274
|
+
attackerPublicKeyPem
|
|
275
|
+
);
|
|
276
|
+
assert.throws(
|
|
277
|
+
() => assertTrustedManifest(forged, createPublisherTrustStore({ publishers: { [publisher.id]: "trusted" } })),
|
|
278
|
+
/trusted signature/
|
|
279
|
+
);
|
|
280
|
+
assert.equal(verifySignedManifest({ ...signed, name: "Tampered" }).ok, false);
|
|
281
|
+
assert.equal(verifySignedManifest({
|
|
282
|
+
...signed,
|
|
283
|
+
trust: { ...signed.trust, signatures: [{ publicKey: "bad-key", signature: "bad-signature" }] }
|
|
284
|
+
}).ok, false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("selects trusted compatible player packs by preference, device, and role", () => {
|
|
288
|
+
const app = appPack();
|
|
289
|
+
const preferred = playerPack({
|
|
290
|
+
id: "com.matterhorn.kanban.mobile",
|
|
291
|
+
recommendedFor: {
|
|
292
|
+
devices: ["mobile"],
|
|
293
|
+
roles: ["member"]
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const fallback = playerPack({ id: "com.matterhorn.kanban.desktop" });
|
|
297
|
+
const incompatible = playerPack({
|
|
298
|
+
id: "com.matterhorn.other",
|
|
299
|
+
supports: [{ appPackId: "com.matterhorn.other", appPackRange: "^1.0.0", appProtocolHash: app.compatibility.appProtocolHash }]
|
|
300
|
+
});
|
|
301
|
+
const untrusted = playerPack({
|
|
302
|
+
id: "com.matterhorn.untrusted",
|
|
303
|
+
publisher: { ...publisher, id: "example.untrusted" }
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
assert.equal(playerSupportsApp(preferred, app), true);
|
|
307
|
+
assert.equal(playerSupportsApp(incompatible, app), false);
|
|
308
|
+
|
|
309
|
+
const selected = choosePlayerPacks({
|
|
310
|
+
appPack: app,
|
|
311
|
+
playerPacks: [fallback, incompatible, preferred, untrusted],
|
|
312
|
+
isTrustedPublisher: (candidate) => candidate.id === "com.matterhorn",
|
|
313
|
+
userPreferences: {
|
|
314
|
+
defaultByAppPack: {
|
|
315
|
+
"com.matterhorn.kanban": "com.matterhorn.kanban.mobile"
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
device: { class: "mobile" },
|
|
319
|
+
actor: { role: "member" }
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
assert.deepEqual(selected.map((player) => player.id), [
|
|
323
|
+
"com.matterhorn.kanban.mobile",
|
|
324
|
+
"com.matterhorn.kanban.desktop"
|
|
325
|
+
]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("matches exact, wildcard, and caret app pack versions", () => {
|
|
329
|
+
assert.equal(satisfiesVersion("1.0.0", "1.0.0"), true);
|
|
330
|
+
assert.equal(satisfiesVersion("1.0.1", "1.0.0"), false);
|
|
331
|
+
assert.equal(satisfiesVersion("2.0.0", "*"), true);
|
|
332
|
+
assert.equal(satisfiesVersion("1.2.0", "^1.0.0"), true);
|
|
333
|
+
assert.equal(satisfiesVersion("2.0.0", "^1.0.0"), false);
|
|
334
|
+
assert.equal(satisfiesVersion("0.9.9", "^1.0.0"), false);
|
|
335
|
+
assert.throws(() => satisfiesVersion("1.0", "^1.0.0"), /version must be a x\.y\.z version/);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("scores room, group, and low-bandwidth player preferences deterministically", () => {
|
|
339
|
+
const app = appPack();
|
|
340
|
+
const lowBandwidth = playerPack({
|
|
341
|
+
id: "com.matterhorn.kanban.low",
|
|
342
|
+
recommendedFor: {
|
|
343
|
+
devices: ["low-bandwidth"]
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
const groupChoice = playerPack({ id: "com.matterhorn.kanban.group" });
|
|
347
|
+
const roomChoice = playerPack({ id: "com.matterhorn.kanban.room" });
|
|
348
|
+
|
|
349
|
+
const selected = choosePlayerPacks({
|
|
350
|
+
room: { id: "room_design" },
|
|
351
|
+
appPack: app,
|
|
352
|
+
playerPacks: [lowBandwidth, groupChoice, roomChoice],
|
|
353
|
+
userPreferences: {
|
|
354
|
+
defaultByRoom: {
|
|
355
|
+
room_design: "com.matterhorn.kanban.room"
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
groupPreferences: {
|
|
359
|
+
recommendedFrontends: {
|
|
360
|
+
"com.matterhorn.kanban": "com.matterhorn.kanban.group"
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
device: {
|
|
364
|
+
class: "desktop",
|
|
365
|
+
lowBandwidth: true
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
assert.deepEqual(selected.map((player) => player.id), [
|
|
370
|
+
"com.matterhorn.kanban.room",
|
|
371
|
+
"com.matterhorn.kanban.group",
|
|
372
|
+
"com.matterhorn.kanban.low"
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("validates optional manifest fields and malformed inputs", () => {
|
|
377
|
+
assert.equal(validateAppPackManifest(appPack({
|
|
378
|
+
compatibility: {
|
|
379
|
+
...appPack().compatibility,
|
|
380
|
+
eventSchemaHash: "sha256-events"
|
|
381
|
+
}
|
|
382
|
+
})).compatibility.eventSchemaHash, "sha256-events");
|
|
383
|
+
|
|
384
|
+
assert.equal(validateHostPackManifest(hostPack({
|
|
385
|
+
runtime: {
|
|
386
|
+
minMatterhornVersion: "0.1.0",
|
|
387
|
+
sandbox: "wasm"
|
|
388
|
+
},
|
|
389
|
+
capabilities: {
|
|
390
|
+
required: ["room.state"],
|
|
391
|
+
optional: ["network.fetch"]
|
|
392
|
+
}
|
|
393
|
+
})).runtime.sandbox, "wasm");
|
|
394
|
+
|
|
395
|
+
assert.equal(validatePlayerPackManifest(playerPack({
|
|
396
|
+
entrypoints: {
|
|
397
|
+
default: "https://apps.matterhorn.gg/default/",
|
|
398
|
+
admin: "https://apps.matterhorn.gg/admin/",
|
|
399
|
+
mobile: "https://apps.matterhorn.gg/mobile/",
|
|
400
|
+
kiosk: "https://apps.matterhorn.gg/kiosk/",
|
|
401
|
+
embedded: "https://apps.matterhorn.gg/embed/"
|
|
402
|
+
},
|
|
403
|
+
supports: [
|
|
404
|
+
{
|
|
405
|
+
appPackId: "com.matterhorn.kanban",
|
|
406
|
+
appPackRange: "^1.0.0",
|
|
407
|
+
appProtocolHash: appProtocolHash(),
|
|
408
|
+
pluginIds: ["@mh-gg/plugin-kanban"]
|
|
409
|
+
}
|
|
410
|
+
],
|
|
411
|
+
recommendedFor: {
|
|
412
|
+
devices: ["tablet"],
|
|
413
|
+
roles: ["admin"],
|
|
414
|
+
groups: ["group_ops"]
|
|
415
|
+
}
|
|
416
|
+
})).supports[0].pluginIds[0], "@mh-gg/plugin-kanban");
|
|
417
|
+
|
|
418
|
+
assert.deepEqual(stripManifestSignatures({ kind: "matterhorn.app" }), { kind: "matterhorn.app" });
|
|
419
|
+
assert.throws(() => validatePlayerPackManifest(null), /player pack must be an object/);
|
|
420
|
+
assert.throws(() => assertManifestIntegrity(appPack(), "not-a-hash"), /expectedHash must be a sha256 hash/);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("loads pack manifests from local refs, workspace refs, and rejects bad integrity", async (t) => {
|
|
424
|
+
const fs = require("node:fs/promises");
|
|
425
|
+
const os = require("node:os");
|
|
426
|
+
const path = require("node:path");
|
|
427
|
+
const {
|
|
428
|
+
loadPackManifest,
|
|
429
|
+
loadPackArtifact,
|
|
430
|
+
normalizePackRef,
|
|
431
|
+
sha256Bytes
|
|
432
|
+
} = require("../src/index.cjs");
|
|
433
|
+
const app = appPack();
|
|
434
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "matterhorn-core-pack-"));
|
|
435
|
+
const file = path.join(dir, "app.json");
|
|
436
|
+
const bytes = Buffer.from(JSON.stringify(app));
|
|
437
|
+
await fs.writeFile(file, bytes);
|
|
438
|
+
|
|
439
|
+
assert.deepEqual(normalizePackRef(file), { url: file });
|
|
440
|
+
const loaded = await loadPackManifest({ url: file, integrity: sha256Bytes(bytes), kind: "matterhorn.app-pack" });
|
|
441
|
+
assert.equal(loaded.ok, true);
|
|
442
|
+
assert.equal(loaded.manifest.id, app.id);
|
|
443
|
+
assert.equal((await loadPackArtifact({ url: file, integrity: sha256Bytes(bytes) })).ok, true);
|
|
444
|
+
|
|
445
|
+
const workspaceLoaded = await loadPackManifest({ url: "workspace:@mh-gg/example#app", integrity: manifestHash(app) }, {
|
|
446
|
+
workspaceResolver(url) {
|
|
447
|
+
assert.equal(url, "workspace:@mh-gg/example#app");
|
|
448
|
+
return app;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
assert.equal(workspaceLoaded.manifestHash, manifestHash(app));
|
|
452
|
+
|
|
453
|
+
const wrong = await loadPackManifest({ url: file, integrity: "sha256-wrong", kind: "matterhorn.app-pack" });
|
|
454
|
+
assert.equal(wrong.ok, false);
|
|
455
|
+
assert.equal(wrong.code, "INTEGRITY_MISMATCH");
|
|
456
|
+
|
|
457
|
+
await fs.writeFile(path.join(dir, "bad.json"), "not json");
|
|
458
|
+
assert.equal((await loadPackManifest({ url: path.join(dir, "bad.json") })).code, "MALFORMED_JSON");
|
|
459
|
+
await fs.writeFile(path.join(dir, "wrong-kind.json"), JSON.stringify({ ...app, kind: "other" }));
|
|
460
|
+
assert.equal((await loadPackManifest({ url: path.join(dir, "wrong-kind.json"), kind: "matterhorn.app-pack" })).code, "MANIFEST_KIND_MISMATCH");
|
|
461
|
+
await assert.rejects(() => loadPackManifest({ url: "ftp://example.com/app.json" }), /ENOENT|unsupported|no such file/i);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("resolves plugin graphs, dependencies, conflicts, and stable hashes", () => {
|
|
465
|
+
const { resolvePluginGraph, pluginIdentity, hashCanonical } = require("../src/index.cjs");
|
|
466
|
+
const roles = { id: "roles", version: "1.0.0", schemas: { state: { descriptor: { roles: true } }, operations: { grant: {} } } };
|
|
467
|
+
const tasks = { id: "tasks", version: "1.2.0", dependencies: { required: [{ id: "roles", range: "^1.0.0" }] }, schemas: { state: {}, operations: {} } };
|
|
468
|
+
const graph = resolvePluginGraph([tasks, roles]);
|
|
469
|
+
assert.deepEqual(graph.plugins.map((plugin) => plugin.id), ["roles", "tasks"]);
|
|
470
|
+
assert.equal(graph.pluginGraphHash, hashCanonical(graph.descriptors));
|
|
471
|
+
assert.equal(pluginIdentity(roles).id, "roles");
|
|
472
|
+
assert.throws(() => resolvePluginGraph([tasks]), /requires missing dependency/);
|
|
473
|
+
assert.throws(() => resolvePluginGraph([roles, { ...tasks, dependencies: { required: [{ id: "roles", range: "^2.0.0" }] } }]), /requires roles/);
|
|
474
|
+
assert.throws(() => resolvePluginGraph([roles, { ...tasks, conflicts: [{ id: "roles" }] }]), /conflicts/);
|
|
475
|
+
const cyclicA = { id: "a", version: "1.0.0", dependencies: { required: [{ id: "b" }] }, schemas: { state: {}, operations: {} } };
|
|
476
|
+
const cyclicB = { id: "b", version: "1.0.0", dependencies: { required: [{ id: "a" }] }, schemas: { state: {}, operations: {} } };
|
|
477
|
+
assert.throws(() => resolvePluginGraph([cyclicA, cyclicB]), /cycle/);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("trust store, capability prompts, and production gates fail closed", () => {
|
|
481
|
+
const {
|
|
482
|
+
createMemoryTrustStore,
|
|
483
|
+
normalizeCapabilities,
|
|
484
|
+
createCapabilityPrompt,
|
|
485
|
+
evaluateCapabilityGrant,
|
|
486
|
+
formatCapabilityAudit,
|
|
487
|
+
enforceProductionPluginPolicy
|
|
488
|
+
} = require("../src/index.cjs");
|
|
489
|
+
const store = createMemoryTrustStore();
|
|
490
|
+
const pub = { id: "com.matterhorn", name: "Matterhorn", publicKey: "key" };
|
|
491
|
+
assert.equal(store.isTrusted(pub), false);
|
|
492
|
+
store.trustPublisher({ publisherId: pub.id, publicKey: pub.publicKey, scope: "user" });
|
|
493
|
+
assert.equal(store.isTrusted(pub), true);
|
|
494
|
+
store.blockPublisher({ publisherId: pub.id, publicKey: pub.publicKey, scope: "group" });
|
|
495
|
+
assert.equal(store.isTrusted(pub, { scope: "group" }), false);
|
|
496
|
+
assert.equal(store.export().length, 2);
|
|
497
|
+
|
|
498
|
+
assert.deepEqual(normalizeCapabilities({ required: ["room.state"], optional: ["room.state", "ai.invoke"] }), { required: ["room.state"], optional: ["ai.invoke"] });
|
|
499
|
+
const prompt = createCapabilityPrompt({ publisher: pub, sources: [{ required: ["room.state", "ai.invoke"], optional: ["relay.route"] }], granted: ["room.state", "relay.route"], denied: ["ai.invoke"] });
|
|
500
|
+
const grant = evaluateCapabilityGrant(prompt);
|
|
501
|
+
assert.equal(grant.ok, false);
|
|
502
|
+
assert.deepEqual(grant.deniedRequired, ["ai.invoke"]);
|
|
503
|
+
assert.match(formatCapabilityAudit(prompt), /high-risk ai\.invoke/);
|
|
504
|
+
assert.throws(() => enforceProductionPluginPolicy({ production: true, publisherTrusted: false }), /untrusted/);
|
|
505
|
+
assert.throws(() => enforceProductionPluginPolicy({ production: true, plugins: [{ id: "bad", reduce: async () => ({}) }], capabilityPrompt: { required: [], optional: [] } }), /async reducer/);
|
|
506
|
+
assert.deepEqual(enforceProductionPluginPolicy({ production: false }), { ok: true, mode: "development" });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
test("creates and verifies pack bundles with pinned manifest integrity", () => {
|
|
511
|
+
const host = hostPack();
|
|
512
|
+
const player = playerPack();
|
|
513
|
+
const app = appPack({
|
|
514
|
+
hostPack: { ...appPack().hostPack, integrity: manifestHash(host) },
|
|
515
|
+
playerPacks: [{
|
|
516
|
+
...appPack().playerPacks[0],
|
|
517
|
+
id: player.id,
|
|
518
|
+
integrity: manifestHash(player)
|
|
519
|
+
}]
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
assert.equal(createAppPackManifest(app).id, app.id);
|
|
523
|
+
assert.equal(createHostPackManifest(host).id, host.id);
|
|
524
|
+
assert.equal(createPlayerPackManifest(player).id, player.id);
|
|
525
|
+
|
|
526
|
+
const bundle = createPackBundle({ appPack: app, hostPack: host, playerPacks: [player] });
|
|
527
|
+
assert.equal(bundle.contract.appPackId, app.id);
|
|
528
|
+
assert.equal(bundle.hostPackHash, manifestHash(host));
|
|
529
|
+
assert.equal(bundle.playerPackHashes[player.id], manifestHash(player));
|
|
530
|
+
assert.equal(verifyPackBundle({ appPack: app, hostPack: host, playerPacks: [player] }).ok, true);
|
|
531
|
+
assert.equal(verifyPackBundle({ appPack: app, hostPack: { ...host, appPackId: "wrong" }, playerPacks: [player] }).ok, false);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("defines player plugins with compatibility metadata", () => {
|
|
535
|
+
const plugin = definePlayerPlugin({
|
|
536
|
+
id: "com.matterhorn.kanban.player-plugin",
|
|
537
|
+
version: "1.0.0",
|
|
538
|
+
supports: [{ appPackId: "com.matterhorn.kanban", appPackRange: "^1.0.0", appProtocolHash: appProtocolHash() }]
|
|
539
|
+
});
|
|
540
|
+
assert.equal(plugin.id, "com.matterhorn.kanban.player-plugin");
|
|
541
|
+
assert.throws(() => definePlayerPlugin({ version: "1.0.0" }), /player plugin.id/);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("loads pack references, resolves plugin graphs, and audits capabilities", async () => {
|
|
545
|
+
const app = appPack();
|
|
546
|
+
const registry = createInMemoryPackRegistry({ "https://registry.example/app.json": app });
|
|
547
|
+
const ref = registry.ref("https://registry.example/app.json");
|
|
548
|
+
const loaded = await loadPackReference(ref, { fetchJson: registry.fetchJson });
|
|
549
|
+
assert.equal(loaded.id, app.id);
|
|
550
|
+
assert.deepEqual(parsePackReferenceUrl("workspace:@mh-gg/example#manifest"), {
|
|
551
|
+
scheme: "workspace",
|
|
552
|
+
packageName: "@mh-gg/example",
|
|
553
|
+
exportName: "manifest"
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const graphPack = hostPack({
|
|
557
|
+
plugins: [
|
|
558
|
+
{ id: "roles", version: "1.0.0", source: "workspace:roles", integrity: "sha256-roles" },
|
|
559
|
+
{ id: "tasks", version: "1.0.0", source: "workspace:tasks", integrity: "sha256-tasks", dependsOn: [{ id: "roles" }] }
|
|
560
|
+
]
|
|
561
|
+
});
|
|
562
|
+
const graph = resolveHostPluginGraph(graphPack, { roles: { id: "roles" }, tasks: { id: "tasks" } });
|
|
563
|
+
assert.deepEqual(graph.refs.map((ref) => ref.id), ["roles", "tasks"]);
|
|
564
|
+
assert.deepEqual(graph.plugins.map((plugin) => plugin.id), ["roles", "tasks"]);
|
|
565
|
+
|
|
566
|
+
const audit = capabilityAudit({ capabilities: { required: ["room.state"], optional: ["relay.mesh"] }, available: ["room.state"] });
|
|
567
|
+
assert.equal(audit.ok, true);
|
|
568
|
+
assert.deepEqual(audit.missingOptional, ["relay.mesh"]);
|
|
569
|
+
|
|
570
|
+
assert.throws(
|
|
571
|
+
() => resolveHostPluginGraph(hostPack({ plugins: [{ id: "tasks", version: "1.0.0", source: "workspace:tasks", integrity: "sha256-tasks", dependsOn: [{ id: "roles" }] }] })),
|
|
572
|
+
/depends on missing plugin roles/
|
|
573
|
+
);
|
|
574
|
+
assert.throws(
|
|
575
|
+
() => resolveHostPluginGraph(hostPack({ plugins: [
|
|
576
|
+
{ id: "a", version: "1.0.0", source: "workspace:a", integrity: "sha256-a", dependsOn: [{ id: "b" }] },
|
|
577
|
+
{ id: "b", version: "1.0.0", source: "workspace:b", integrity: "sha256-b", dependsOn: [{ id: "a" }] }
|
|
578
|
+
] })),
|
|
579
|
+
/dependency cycle/
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("publisher trust and production plugin gates fail closed", () => {
|
|
584
|
+
const trustStore = createPublisherTrustStore({ publishers: { "com.matterhorn.kanban.host": "trusted" } });
|
|
585
|
+
assert.equal(trustStore.isTrusted("com.matterhorn.kanban.host"), true);
|
|
586
|
+
trustStore.block("bad.publisher");
|
|
587
|
+
assert.equal(trustStore.isBlocked("bad.publisher"), true);
|
|
588
|
+
|
|
589
|
+
const safe = hostPack({ runtime: { minMatterhornVersion: "0.1.0", sandbox: "process" } });
|
|
590
|
+
assert.equal(assertProductionPluginGates({
|
|
591
|
+
hostPack: safe,
|
|
592
|
+
trustStore,
|
|
593
|
+
requireSignature: false,
|
|
594
|
+
availableCapabilities: ["room.state", "room.roles"]
|
|
595
|
+
}).ok, true);
|
|
596
|
+
|
|
597
|
+
assert.throws(() => assertTrustedManifest(appPack(), undefined, { requireSignature: false }), /not trusted/);
|
|
598
|
+
|
|
599
|
+
assert.throws(() => assertProductionPluginGates({
|
|
600
|
+
hostPack: safe,
|
|
601
|
+
requireSignature: false,
|
|
602
|
+
availableCapabilities: ["room.state", "room.roles"]
|
|
603
|
+
}), /trust store is required/);
|
|
604
|
+
|
|
605
|
+
assert.throws(() => assertProductionPluginGates({
|
|
606
|
+
hostPack: hostPack({ runtime: { minMatterhornVersion: "0.1.0", sandbox: "none" } }),
|
|
607
|
+
trustStore,
|
|
608
|
+
requireSignature: false,
|
|
609
|
+
availableCapabilities: ["room.state", "room.roles"]
|
|
610
|
+
}), /without a sandbox/);
|
|
611
|
+
|
|
612
|
+
assert.throws(() => assertProductionPluginGates({
|
|
613
|
+
hostPack: safe,
|
|
614
|
+
trustStore,
|
|
615
|
+
availableCapabilities: ["room.state", "room.roles"]
|
|
616
|
+
}), /publisher is required/);
|
|
617
|
+
|
|
618
|
+
assert.throws(() => assertTrustedManifest(appPack(), createPublisherTrustStore()), /not trusted/);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("covers pack-loading, trust, registry, and host-plugin graph edge cases", async () => {
|
|
622
|
+
const fs = require("node:fs/promises");
|
|
623
|
+
const os = require("node:os");
|
|
624
|
+
const path = require("node:path");
|
|
625
|
+
const {
|
|
626
|
+
createMemoryTrustStore,
|
|
627
|
+
loadPackArtifact,
|
|
628
|
+
loadPackManifest,
|
|
629
|
+
normalizePackRef,
|
|
630
|
+
sha256Bytes
|
|
631
|
+
} = require("../src/index.cjs");
|
|
632
|
+
|
|
633
|
+
const app = appPack();
|
|
634
|
+
const host = hostPack({
|
|
635
|
+
plugins: [
|
|
636
|
+
{ id: "base", version: "1.0.0", source: "workspace:base", integrity: "sha256-base", capabilities: { required: ["room.state"], optional: ["relay.route"] } },
|
|
637
|
+
{ id: "optional", version: "1.0.0", source: "workspace:optional", integrity: "sha256-optional", dependsOn: [{ id: "missing", optional: true }] }
|
|
638
|
+
]
|
|
639
|
+
});
|
|
640
|
+
const graph = resolveHostPluginGraph(host, { base: { id: "base" }, optional: { id: "optional" } });
|
|
641
|
+
assert.deepEqual(graph.refs.map((ref) => ref.id), ["base", "optional"]);
|
|
642
|
+
assert.deepEqual(graph.refs[0].capabilities.required, ["room.state"]);
|
|
643
|
+
|
|
644
|
+
assert.throws(() => resolveHostPluginGraph(hostPack({ plugins: [
|
|
645
|
+
{ id: "a", version: "1.0.0", source: "workspace:a", integrity: "sha256-a", conflictsWith: ["b"] },
|
|
646
|
+
{ id: "b", version: "1.0.0", source: "workspace:b", integrity: "sha256-b" }
|
|
647
|
+
] })), /conflicts/);
|
|
648
|
+
assert.throws(() => resolveHostPluginGraph(hostPack({ plugins: [
|
|
649
|
+
{ id: "a", version: "1.0.0", source: "workspace:a", integrity: "sha256-a", dependsOn: [{ id: "b", version: "2.0.0" }] },
|
|
650
|
+
{ id: "b", version: "1.0.0", source: "workspace:b", integrity: "sha256-b" }
|
|
651
|
+
] })), /depends on b@2\.0\.0/);
|
|
652
|
+
assert.throws(() => parsePackReferenceUrl("workspace:"), /missing a package/);
|
|
653
|
+
assert.throws(() => parsePackReferenceUrl("file:"), /missing a path/);
|
|
654
|
+
assert.throws(() => parsePackReferenceUrl("ipfs://abc"), /Unsupported pack reference/);
|
|
655
|
+
|
|
656
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "matterhorn-core-extra-"));
|
|
657
|
+
const appFile = path.join(dir, "app.json");
|
|
658
|
+
await fs.writeFile(appFile, JSON.stringify(app));
|
|
659
|
+
const fileUrlLoaded = await loadPackReference({ url: `file:${appFile}`, integrity: manifestHash(app) });
|
|
660
|
+
assert.equal(fileUrlLoaded.id, app.id);
|
|
661
|
+
|
|
662
|
+
const exportFile = path.join(dir, "exports.json");
|
|
663
|
+
await fs.writeFile(exportFile, JSON.stringify({ app }));
|
|
664
|
+
const fileExportLoaded = await loadPackReference({ url: `file:${exportFile}#app`, integrity: manifestHash(app) });
|
|
665
|
+
assert.equal(fileExportLoaded.id, app.id);
|
|
666
|
+
|
|
667
|
+
const httpBytes = Buffer.from(JSON.stringify(app));
|
|
668
|
+
const httpArtifact = await loadPackArtifact({ url: "https://registry.example/app.json", integrity: sha256Bytes(httpBytes) }, {
|
|
669
|
+
fetch: async () => ({ ok: true, arrayBuffer: async () => httpBytes })
|
|
670
|
+
});
|
|
671
|
+
assert.equal(httpArtifact.ok, true);
|
|
672
|
+
const httpTextArtifact = await loadPackArtifact({ url: "https://registry.example/app-text.json" }, {
|
|
673
|
+
allowRemoteWithoutIntegrity: true,
|
|
674
|
+
fetch: async () => ({ ok: true, text: async () => JSON.stringify(app) })
|
|
675
|
+
});
|
|
676
|
+
assert.equal(httpTextArtifact.ok, true);
|
|
677
|
+
await assert.rejects(() => loadPackArtifact({ url: "https://registry.example/missing.json", integrity: "sha256-missing" }, { fetch: async () => ({ ok: false }) }), /Unable to fetch/);
|
|
678
|
+
|
|
679
|
+
await fs.writeFile(path.join(dir, "weird.json"), JSON.stringify({ kind: "matterhorn.weird" }));
|
|
680
|
+
const invalidKind = await loadPackManifest({ url: path.join(dir, "weird.json") });
|
|
681
|
+
assert.equal(invalidKind.code, "MANIFEST_INVALID");
|
|
682
|
+
await fs.writeFile(path.join(dir, "empty.json"), JSON.stringify({}));
|
|
683
|
+
assert.equal((await loadPackManifest({ url: path.join(dir, "empty.json") })).code, "MANIFEST_KIND_MISSING");
|
|
684
|
+
assert.deepEqual(normalizePackRef({ url: appFile, kind: APP_PACK_KIND }), { url: appFile, kind: APP_PACK_KIND });
|
|
685
|
+
|
|
686
|
+
const workspaceLoaded = await loadPackReference({ url: "workspace:@mh-gg/core-edge#app", integrity: manifestHash(app) }, { workspace: { "@mh-gg/core-edge": { app } } });
|
|
687
|
+
assert.equal(workspaceLoaded.id, app.id);
|
|
688
|
+
await assert.rejects(() => loadPackReference({ url: "workspace:@mh-gg/core-edge#missing", integrity: manifestHash(app) }, { workspace: { "@mh-gg/core-edge": { app } } }), /does not export/);
|
|
689
|
+
await assert.rejects(() => loadPackReference({ url: "workspace:@mh-gg/not-registered", integrity: manifestHash(app) }, { workspace: {} }), /not registered/);
|
|
690
|
+
const fetched = await loadPackReference({ url: "https://registry.example/app.json#app", integrity: manifestHash(app) }, { fetchJson: async () => ({ app }) });
|
|
691
|
+
assert.equal(fetched.id, app.id);
|
|
692
|
+
await assert.rejects(() => loadPackReference({ url: "https://registry.example/app.json", integrity: manifestHash(app) }), /fetchJson is required/);
|
|
693
|
+
|
|
694
|
+
const memoryTrust = createMemoryTrustStore([{ publisherId: publisher.id, publicKey: "*", scope: "user" }]);
|
|
695
|
+
assert.equal(memoryTrust.isTrusted({ ...publisher, publicKey: "another-key" }), true);
|
|
696
|
+
assert.equal(memoryTrust.isTrusted(null), false);
|
|
697
|
+
assert.equal(memoryTrust.blockPublisher({ publisherId: publisher.id, publicKey: "another-key" }).blocked, true);
|
|
698
|
+
assert.equal(memoryTrust.isTrusted({ ...publisher, publicKey: "another-key" }), false);
|
|
699
|
+
|
|
700
|
+
const publisherTrust = createPublisherTrustStore({ publishers: { "reviewed.pub": { status: "review", note: "manual review" } } });
|
|
701
|
+
assert.equal(publisherTrust.status("reviewed.pub"), "review");
|
|
702
|
+
publisherTrust.trust("trusted.pub");
|
|
703
|
+
publisherTrust.review("review.pub");
|
|
704
|
+
assert.equal(publisherTrust.snapshot().publishers["trusted.pub"].status, "trusted");
|
|
705
|
+
assert.throws(() => createPublisherTrustStore({ publishers: { "bad.pub": "unknown" } }), /Invalid trust status/);
|
|
706
|
+
|
|
707
|
+
const registry = createInMemoryPackRegistry();
|
|
708
|
+
const published = registry.publish("https://registry.example/published.json", app);
|
|
709
|
+
assert.equal(published.integrity, manifestHash(app));
|
|
710
|
+
assert.deepEqual(registry.list(), ["https://registry.example/published.json"]);
|
|
711
|
+
assert.equal((await registry.fetchJson(published.url)).id, app.id);
|
|
712
|
+
assert.equal(registry.ref(published.url).integrity, manifestHash(app));
|
|
713
|
+
assert.throws(() => registry.ref("missing"), /not found/);
|
|
714
|
+
await assert.rejects(() => registry.fetchJson("missing"), /not found/);
|
|
715
|
+
|
|
716
|
+
const blockedTrust = createPublisherTrustStore({ publishers: { [app.publisher.id]: "blocked" } });
|
|
717
|
+
assert.throws(() => assertTrustedManifest(app, blockedTrust, { requireSignature: false }), /blocked/);
|
|
718
|
+
const reviewTrust = createPublisherTrustStore({ publishers: { [app.publisher.id]: "review" } });
|
|
719
|
+
assert.equal(assertTrustedManifest(app, reviewTrust, { requireTrusted: false, requireSignature: false }).status, "review");
|
|
720
|
+
assert.throws(() => assertTrustedManifest(app, createPublisherTrustStore({ publishers: { [app.publisher.id]: "trusted" } })), /trusted signature/);
|
|
721
|
+
|
|
722
|
+
const localTrustStore = createPublisherTrustStore({ publishers: { "com.matterhorn.kanban.host": "trusted" } });
|
|
723
|
+
const missingCapabilityGate = assertProductionPluginGates({
|
|
724
|
+
hostPack: hostPack({ capabilities: { required: ["network.fetch"], optional: [] }, runtime: { minMatterhornVersion: "0.1.0", sandbox: "process" } }),
|
|
725
|
+
trustStore: localTrustStore,
|
|
726
|
+
requireSignature: false,
|
|
727
|
+
availableCapabilities: [],
|
|
728
|
+
allowMissingCapabilities: true
|
|
729
|
+
});
|
|
730
|
+
assert.equal(missingCapabilityGate.capabilityAudit.ok, false);
|
|
731
|
+
});
|