@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 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,