@fabricorg/assembly 0.3.0 → 0.4.1
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/CHANGELOG.md +35 -0
- package/README.md +12 -0
- package/dist/index.cjs +164 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -3
- package/dist/index.d.ts +75 -3
- package/dist/index.js +162 -5
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @fabricorg/assembly
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 715e920: The adapter plane. An assembly manifest may declare `adapters`, each structurally the output of `certifyAdapter`, and resolution judges every port a contract requires by the deployment gate's own rule through `validatePortRequirements` with certification required: `unsatisfied_port`, `uncertified_adapter`, `standard_not_spoken`, `version_mismatch`, and, when `portDefinitions` are supplied, `stale_certification`. The lockfile records each certified adapter with the port version it was certified against and a digest of the certification, and the assembly digest covers them. Declaring `adapters`, even empty, engages the plane; an assembly that does not declare it resolves exactly as before, digest included. Every declared adapter is recorded in the lockfile with `certified` true or false, `portDefinitions` are validated before use, and `assertAssemblyRuntimeCompatible` accepts the runtime's registered adapters and refuses any drift from the approved list. Disclosed: this adds `@fabricorg/ports` as a runtime dependency and several exported types and one function; it is released as a patch because a minor on this 0.x package would cascade a major onto every peer, and nothing existing changes behaviour.
|
|
8
|
+
- Updated dependencies [764b94b]
|
|
9
|
+
- @fabricorg/ports@0.5.4
|
|
10
|
+
|
|
11
|
+
## 0.4.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 274d948: Close the gaps a second independent review found.
|
|
16
|
+
|
|
17
|
+
Experience promotion now binds the usage contracts it compiles routes from to the assembly the
|
|
18
|
+
release was validated against. Promotion previously accepted arbitrary contracts while copying the
|
|
19
|
+
release's assembly digest through unchanged, so a substituted contract could remap an already-granted
|
|
20
|
+
`capability://` reference onto a different installed action. PlatformHost still authorized that
|
|
21
|
+
action, which made this an integrity failure of what a release exposes rather than an authorization
|
|
22
|
+
bypass, and invisible downstream. `@fabricorg/assembly` exports `computeAssemblyLockfileDigest` so
|
|
23
|
+
tooling can build a self-consistent lockfile; the validator already recomputed it.
|
|
24
|
+
|
|
25
|
+
The MFE loader gains `fabric.mfe-loader.mismatched-digest-denial.v1`, which supplies a well-formed
|
|
26
|
+
integrity value that cannot match the approved bundle. The previous integrity check supplied only a
|
|
27
|
+
malformed value, so it proved a regex ran; a transport that ignored integrity entirely certified
|
|
28
|
+
clean. Since same-realm MFE code carries the application's own authority, a false integrity
|
|
29
|
+
certification is worth catching in the suite rather than in production.
|
|
30
|
+
|
|
31
|
+
The SDUI channel suite no longer certifies a channel it never exercised: the missing-vocabulary check
|
|
32
|
+
now runs against the subject rather than a fresh built-in one, rejects a vacuous or contradictory
|
|
33
|
+
subject instead of passing it, requires a refusal to name the digest so an unrelated crash cannot
|
|
34
|
+
read as correct behaviour, and edits several digest-covered fields so a channel cannot pass by
|
|
35
|
+
special-casing one fixture. `supportedTokenSets` uses a total comparator over its fields rather than
|
|
36
|
+
an interpolated key, and duplicate or malformed token-set references are rejected.
|
|
37
|
+
|
|
3
38
|
## 0.3.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -68,3 +68,15 @@ const lockfile = assertAssemblyResolved({
|
|
|
68
68
|
It does not load live modules, execute handlers, or run inside
|
|
69
69
|
`@fabricorg/platform`. The assembly package depends on `@fabricorg/adoption-bindings`
|
|
70
70
|
and `@fabricorg/gen-capability` for contract and binding validation.
|
|
71
|
+
|
|
72
|
+
## The adapter plane
|
|
73
|
+
|
|
74
|
+
An assembly manifest may declare `adapters`, each the output of `certifyAdapter`. Declaring the
|
|
75
|
+
field, even empty, engages the plane: every port a contract requires must be satisfied by a declared,
|
|
76
|
+
certified adapter, judged by `validatePortRequirements` with certification required, and a
|
|
77
|
+
certification against an older port contract is stale when `portDefinitions` are supplied. The
|
|
78
|
+
lockfile records every declared adapter, certified or not, and the assembly digest covers them.
|
|
79
|
+
`assertAssemblyRuntimeCompatible(lockfile, capabilities, registeredAdapters)` then checks that what a
|
|
80
|
+
runtime registered behind each port is what the assembly approved. An assembly that does not declare
|
|
81
|
+
`adapters` resolves exactly as before.
|
|
82
|
+
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,8 @@ __export(index_exports, {
|
|
|
27
27
|
assertAssemblyResolved: () => assertAssemblyResolved,
|
|
28
28
|
assertAssemblyRuntimeCompatible: () => assertAssemblyRuntimeCompatible,
|
|
29
29
|
assertComponentPackManifest: () => assertComponentPackManifest,
|
|
30
|
+
computeAdapterCertificationDigest: () => computeAdapterCertificationDigest,
|
|
31
|
+
computeAssemblyLockfileDigest: () => computeAssemblyLockfileDigest,
|
|
30
32
|
computeUsageContractDocumentDigest: () => computeUsageContractDocumentDigest,
|
|
31
33
|
parseSemverRange: () => parseSemverRange,
|
|
32
34
|
resolveAssembly: () => resolveAssembly,
|
|
@@ -36,6 +38,7 @@ __export(index_exports, {
|
|
|
36
38
|
});
|
|
37
39
|
module.exports = __toCommonJS(index_exports);
|
|
38
40
|
var import_node_crypto = require("crypto");
|
|
41
|
+
var import_ports = require("@fabricorg/ports");
|
|
39
42
|
var import_adoption_bindings = require("@fabricorg/adoption-bindings");
|
|
40
43
|
var import_gen_capability = require("@fabricorg/gen-capability");
|
|
41
44
|
var ASSEMBLY_MANIFEST_FORMAT_VERSION = 2;
|
|
@@ -224,15 +227,24 @@ function computeUsageContractDocumentDigest(document) {
|
|
|
224
227
|
(0, import_gen_capability.assertUsageContractDocument)(document);
|
|
225
228
|
return assemblyDigest(document);
|
|
226
229
|
}
|
|
230
|
+
function computeAssemblyLockfileDigest(lockfile) {
|
|
231
|
+
return assemblyDigest(lockfileDigestBody(lockfile));
|
|
232
|
+
}
|
|
227
233
|
function lockfileDigestBody(lockfile) {
|
|
228
234
|
return {
|
|
229
235
|
formatVersion: lockfile.formatVersion,
|
|
230
236
|
application: lockfile.application,
|
|
231
237
|
capabilities: lockfile.capabilities,
|
|
232
238
|
componentPacks: lockfile.componentPacks,
|
|
233
|
-
bindings: lockfile.bindings
|
|
239
|
+
bindings: lockfile.bindings,
|
|
240
|
+
// Omitted when absent, so a lockfile from before the adapter plane
|
|
241
|
+
// keeps the digest it was approved with.
|
|
242
|
+
...lockfile.adapters ? { adapters: lockfile.adapters } : {}
|
|
234
243
|
};
|
|
235
244
|
}
|
|
245
|
+
function computeAdapterCertificationDigest(certification) {
|
|
246
|
+
return assemblyDigest({ ...certification, checks: [...certification.checks].sort() });
|
|
247
|
+
}
|
|
236
248
|
function fail(path, message) {
|
|
237
249
|
throw new Error(`Assembly manifest ${path} ${message}.`);
|
|
238
250
|
}
|
|
@@ -278,6 +290,42 @@ function assertAssemblyManifest(value, path = "assembly") {
|
|
|
278
290
|
fail(`${path}.contracts[${index}].document`, error instanceof Error ? error.message : String(error));
|
|
279
291
|
}
|
|
280
292
|
});
|
|
293
|
+
if (manifest.adapters !== void 0) {
|
|
294
|
+
requireArray(manifest.adapters, `${path}.adapters`).forEach((entry, index) => {
|
|
295
|
+
assertAdapterEntry(entry, `${path}.adapters[${index}]`);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function assertAdapterEntry(value, path) {
|
|
300
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "must be an object");
|
|
301
|
+
const adapter = value;
|
|
302
|
+
requireString(adapter.port, `${path}.port`);
|
|
303
|
+
requireString(adapter.vendor, `${path}.vendor`);
|
|
304
|
+
if (adapter.version !== void 0) requireString(adapter.version, `${path}.version`);
|
|
305
|
+
if (adapter.standard !== void 0) {
|
|
306
|
+
const standard = adapter.standard;
|
|
307
|
+
if (!standard || typeof standard !== "object" || Array.isArray(standard)) fail(`${path}.standard`, "must be an object");
|
|
308
|
+
requireString(standard.name, `${path}.standard.name`);
|
|
309
|
+
if (standard.version !== void 0) requireString(standard.version, `${path}.standard.version`);
|
|
310
|
+
}
|
|
311
|
+
if (adapter.certification !== void 0) {
|
|
312
|
+
const certification = adapter.certification;
|
|
313
|
+
if (!certification || typeof certification !== "object" || Array.isArray(certification)) fail(`${path}.certification`, "must be an object");
|
|
314
|
+
requireString(certification.portId, `${path}.certification.portId`);
|
|
315
|
+
if (!/^\d+\.\d+\.\d+$/.test(requireString(certification.portVersion, `${path}.certification.portVersion`))) {
|
|
316
|
+
fail(`${path}.certification.portVersion`, "must be a semver version");
|
|
317
|
+
}
|
|
318
|
+
const checks = requireArray(certification.checks, `${path}.certification.checks`);
|
|
319
|
+
if (checks.length === 0 || !checks.every((check) => typeof check === "string" && check.trim())) {
|
|
320
|
+
fail(`${path}.certification.checks`, "must name at least one check");
|
|
321
|
+
}
|
|
322
|
+
if (Number.isNaN(Date.parse(requireString(certification.certifiedAt, `${path}.certification.certifiedAt`)))) {
|
|
323
|
+
fail(`${path}.certification.certifiedAt`, "must be an instant");
|
|
324
|
+
}
|
|
325
|
+
if (certification.portId !== adapter.port) {
|
|
326
|
+
fail(`${path}.certification.portId`, `must match the adapter's port "${String(adapter.port)}"`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
281
329
|
}
|
|
282
330
|
function assertComponentPackManifest(value, path = "pack") {
|
|
283
331
|
if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "must be an object");
|
|
@@ -369,19 +417,77 @@ function assertAssemblyLockfile(value, path = "lockfile") {
|
|
|
369
417
|
if (binding?.valid !== true) fail(`${path}.bindings[${index}].valid`, "must be true");
|
|
370
418
|
requireArray(binding?.findings, `${path}.bindings[${index}].findings`);
|
|
371
419
|
}
|
|
420
|
+
let adapters;
|
|
421
|
+
if (lockfile.adapters !== void 0) {
|
|
422
|
+
adapters = requireArray(lockfile.adapters, `${path}.adapters`);
|
|
423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
424
|
+
for (const [index, entry] of adapters.entries()) {
|
|
425
|
+
const adapter = entry;
|
|
426
|
+
const port = requireString(adapter?.port, `${path}.adapters[${index}].port`);
|
|
427
|
+
const vendor = requireString(adapter?.vendor, `${path}.adapters[${index}].vendor`);
|
|
428
|
+
const key = `${port}/${vendor}`;
|
|
429
|
+
if (seen.has(key)) fail(`${path}.adapters[${index}]`, `declares adapter "${key}" a second time`);
|
|
430
|
+
seen.add(key);
|
|
431
|
+
if (adapter?.version !== void 0) requireString(adapter.version, `${path}.adapters[${index}].version`);
|
|
432
|
+
if (adapter?.standard !== void 0) {
|
|
433
|
+
const standard = adapter.standard;
|
|
434
|
+
if (!standard || typeof standard !== "object" || Array.isArray(standard)) fail(`${path}.adapters[${index}].standard`, "must be an object");
|
|
435
|
+
requireString(standard.name, `${path}.adapters[${index}].standard.name`);
|
|
436
|
+
if (standard.version !== void 0) requireString(standard.version, `${path}.adapters[${index}].standard.version`);
|
|
437
|
+
}
|
|
438
|
+
if (typeof adapter?.certified !== "boolean") fail(`${path}.adapters[${index}].certified`, "must be a boolean");
|
|
439
|
+
if (adapter.certified) {
|
|
440
|
+
if (!/^\d+\.\d+\.\d+$/.test(requireString(adapter?.portVersion, `${path}.adapters[${index}].portVersion`))) {
|
|
441
|
+
fail(`${path}.adapters[${index}].portVersion`, "must be a semver version");
|
|
442
|
+
}
|
|
443
|
+
if (Number.isNaN(Date.parse(requireString(adapter?.certifiedAt, `${path}.adapters[${index}].certifiedAt`)))) {
|
|
444
|
+
fail(`${path}.adapters[${index}].certifiedAt`, "must be an instant");
|
|
445
|
+
}
|
|
446
|
+
if (!/^[0-9a-f]{64}$/.test(requireString(adapter?.certificationDigest, `${path}.adapters[${index}].certificationDigest`))) {
|
|
447
|
+
fail(`${path}.adapters[${index}].certificationDigest`, "must be a lowercase SHA-256 digest");
|
|
448
|
+
}
|
|
449
|
+
} else if (adapter.portVersion !== void 0 || adapter.certifiedAt !== void 0 || adapter.certificationDigest !== void 0) {
|
|
450
|
+
fail(`${path}.adapters[${index}]`, "carries certification fields but is marked uncertified");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
372
454
|
const expected = assemblyDigest(lockfileDigestBody({
|
|
373
455
|
formatVersion: ASSEMBLY_LOCKFILE_FORMAT_VERSION,
|
|
374
456
|
application: lockfile.application,
|
|
375
457
|
capabilities,
|
|
376
458
|
componentPacks,
|
|
377
|
-
bindings
|
|
459
|
+
bindings,
|
|
460
|
+
...adapters ? { adapters } : {}
|
|
378
461
|
}));
|
|
379
462
|
if (lockfile.assemblyDigest !== expected) {
|
|
380
463
|
fail(`${path}.assemblyDigest`, `does not match resolved lockfile content; expected "${expected}"`);
|
|
381
464
|
}
|
|
382
465
|
}
|
|
383
|
-
function assertAssemblyRuntimeCompatible(lockfile, loadedCapabilities) {
|
|
466
|
+
function assertAssemblyRuntimeCompatible(lockfile, loadedCapabilities, registeredAdapters) {
|
|
384
467
|
assertAssemblyLockfile(lockfile);
|
|
468
|
+
if (lockfile.adapters !== void 0 && registeredAdapters !== void 0) {
|
|
469
|
+
const registeredByKey = new Map(registeredAdapters.map((adapter) => [`${adapter.port}/${adapter.vendor}`, adapter]));
|
|
470
|
+
for (const approved of lockfile.adapters) {
|
|
471
|
+
const key = `${approved.port}/${approved.vendor}`;
|
|
472
|
+
const registered = registeredByKey.get(key);
|
|
473
|
+
if (!registered) throw new Error(`Approved adapter "${key}" is not registered by this runtime.`);
|
|
474
|
+
if (approved.version !== void 0 && registered.version !== approved.version) {
|
|
475
|
+
throw new Error(`Registered adapter "${key}" is version "${registered.version ?? "unversioned"}"; the assembly approved "${approved.version}".`);
|
|
476
|
+
}
|
|
477
|
+
if (approved.certified) {
|
|
478
|
+
const digest = registered.certification ? computeAdapterCertificationDigest(registered.certification) : void 0;
|
|
479
|
+
if (digest !== approved.certificationDigest) {
|
|
480
|
+
throw new Error(`Registered adapter "${key}" does not carry the certification the assembly approved (digest "${approved.certificationDigest}").`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
for (const registered of registeredAdapters) {
|
|
485
|
+
const key = `${registered.port}/${registered.vendor}`;
|
|
486
|
+
if (!lockfile.adapters.some((approved) => approved.port === registered.port && approved.vendor === registered.vendor)) {
|
|
487
|
+
throw new Error(`Registered adapter "${key}" is not approved by assembly "${lockfile.assemblyDigest}".`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
385
491
|
const loadedByNamespace = /* @__PURE__ */ new Map();
|
|
386
492
|
for (const loaded of loadedCapabilities) {
|
|
387
493
|
if (loadedByNamespace.has(loaded.namespace)) {
|
|
@@ -586,6 +692,51 @@ function resolveAssembly(input) {
|
|
|
586
692
|
}
|
|
587
693
|
contractsByNamespace.set(entry.namespace, entry.document.capability);
|
|
588
694
|
}
|
|
695
|
+
const resolvedAdapters = [];
|
|
696
|
+
const declaredAdapters = input.manifest.adapters ?? [];
|
|
697
|
+
const adapterKeys = /* @__PURE__ */ new Set();
|
|
698
|
+
for (const adapter of declaredAdapters) {
|
|
699
|
+
const key = `${adapter.port}/${adapter.vendor}`;
|
|
700
|
+
if (adapterKeys.has(key)) {
|
|
701
|
+
add("duplicate_adapter", `assembly.adapters.${key}`, `adapter "${key}" is declared a second time`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
adapterKeys.add(key);
|
|
705
|
+
resolvedAdapters.push({
|
|
706
|
+
port: adapter.port,
|
|
707
|
+
vendor: adapter.vendor,
|
|
708
|
+
...adapter.version === void 0 ? {} : { version: adapter.version },
|
|
709
|
+
...adapter.standard === void 0 ? {} : { standard: adapter.standard },
|
|
710
|
+
certified: adapter.certification !== void 0,
|
|
711
|
+
...adapter.certification === void 0 ? {} : {
|
|
712
|
+
portVersion: adapter.certification.portVersion,
|
|
713
|
+
certifiedAt: adapter.certification.certifiedAt,
|
|
714
|
+
certificationDigest: computeAdapterCertificationDigest(adapter.certification)
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
const portDefinitions = (input.portDefinitions ?? []).map((definition, index) => {
|
|
719
|
+
if (typeof definition.id !== "string" || !import_ports.PORT_ID.test(definition.id)) {
|
|
720
|
+
fail(`portDefinitions[${index}].id`, `must be a namespaced lowercase port id, received ${JSON.stringify(definition.id)}`);
|
|
721
|
+
}
|
|
722
|
+
if (typeof definition.version !== "string" || !/^\d+\.\d+\.\d+$/.test(definition.version)) {
|
|
723
|
+
fail(`portDefinitions[${index}].version`, `must be a semver version, received ${JSON.stringify(definition.version)}`);
|
|
724
|
+
}
|
|
725
|
+
return { id: definition.id, version: definition.version, description: "", checks: () => [] };
|
|
726
|
+
});
|
|
727
|
+
for (const [namespace, capability] of contractsByNamespace) {
|
|
728
|
+
if (input.manifest.adapters === void 0) break;
|
|
729
|
+
if (!capability.requirements?.ports?.length) continue;
|
|
730
|
+
const result = (0, import_ports.validatePortRequirements)({
|
|
731
|
+
capability: { namespace, requirements: { ports: capability.requirements.ports } },
|
|
732
|
+
adapters: declaredAdapters,
|
|
733
|
+
definitions: portDefinitions,
|
|
734
|
+
requireCertification: true
|
|
735
|
+
});
|
|
736
|
+
for (const finding of result.findings) {
|
|
737
|
+
add(finding.code, `assembly.contracts.${namespace}.requirements.ports.${finding.port}`, finding.message);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
589
740
|
const resolvedBindings = [];
|
|
590
741
|
for (const entry of input.manifest.bindings) {
|
|
591
742
|
const contract = contractsByNamespace.get(entry.capability);
|
|
@@ -636,12 +787,17 @@ function resolveAssembly(input) {
|
|
|
636
787
|
const bindings = [...resolvedBindings].sort(
|
|
637
788
|
(a, b) => a.capability < b.capability ? -1 : a.capability > b.capability ? 1 : 0
|
|
638
789
|
);
|
|
790
|
+
const adapters = input.manifest.adapters === void 0 ? void 0 : [...resolvedAdapters].sort((a, b) => {
|
|
791
|
+
const key = (e) => `${e.port}/${e.vendor}`;
|
|
792
|
+
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
|
|
793
|
+
});
|
|
639
794
|
const lockfileBody = {
|
|
640
795
|
formatVersion: ASSEMBLY_LOCKFILE_FORMAT_VERSION,
|
|
641
796
|
application: input.manifest.application,
|
|
642
797
|
capabilities,
|
|
643
798
|
componentPacks,
|
|
644
|
-
bindings
|
|
799
|
+
bindings,
|
|
800
|
+
...adapters ? { adapters } : {}
|
|
645
801
|
};
|
|
646
802
|
const lockfile = {
|
|
647
803
|
formatVersion: ASSEMBLY_LOCKFILE_FORMAT_VERSION,
|
|
@@ -649,7 +805,8 @@ function resolveAssembly(input) {
|
|
|
649
805
|
assemblyDigest: assemblyDigest(lockfileDigestBody(lockfileBody)),
|
|
650
806
|
capabilities,
|
|
651
807
|
componentPacks,
|
|
652
|
-
bindings
|
|
808
|
+
bindings,
|
|
809
|
+
...adapters ? { adapters } : {}
|
|
653
810
|
};
|
|
654
811
|
return {
|
|
655
812
|
lockfile: findings.length === 0 ? lockfile : void 0,
|
|
@@ -725,6 +882,8 @@ function validateAssemblyBindings(input) {
|
|
|
725
882
|
assertAssemblyResolved,
|
|
726
883
|
assertAssemblyRuntimeCompatible,
|
|
727
884
|
assertComponentPackManifest,
|
|
885
|
+
computeAdapterCertificationDigest,
|
|
886
|
+
computeAssemblyLockfileDigest,
|
|
728
887
|
computeUsageContractDocumentDigest,
|
|
729
888
|
parseSemverRange,
|
|
730
889
|
resolveAssembly,
|