@xemahq/xema 0.48.0 → 0.49.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.
Files changed (98) hide show
  1. package/dist/assets/appliance-base/distribution.lock.json +3936 -1515
  2. package/dist/assets/appliance-base/images.lock.json +878 -1161
  3. package/dist/assets/appliance-lean/distribution.lock.json +3663 -916
  4. package/dist/assets/appliance-lean/images.lock.json +825 -924
  5. package/dist/assets/boot-graph.json +398 -93
  6. package/dist/assets/distribution-assets.manifest.json +10 -10
  7. package/dist/commands/edition.d.ts.map +1 -1
  8. package/dist/commands/edition.js +5 -3
  9. package/dist/commands/edition.js.map +1 -1
  10. package/dist/dev/env-defaults.d.ts.map +1 -1
  11. package/dist/dev/env-defaults.js +0 -1
  12. package/dist/dev/env-defaults.js.map +1 -1
  13. package/dist/edition/verify-image-evidence.bundle.mjs +4386 -2503
  14. package/dist/edition/verify-image-evidence.bundle.provenance.json +590 -488
  15. package/dist/runtime/auth/cli-application.d.ts +18 -0
  16. package/dist/runtime/auth/cli-application.d.ts.map +1 -0
  17. package/dist/runtime/auth/cli-application.js +39 -0
  18. package/dist/runtime/auth/cli-application.js.map +1 -0
  19. package/dist/runtime/edition/verb-map.d.ts +3 -1
  20. package/dist/runtime/edition/verb-map.d.ts.map +1 -1
  21. package/dist/runtime/edition/verb-map.js +5 -1
  22. package/dist/runtime/edition/verb-map.js.map +1 -1
  23. package/dist/runtime/env-seeder.d.ts.map +1 -1
  24. package/dist/runtime/env-seeder.js +21 -0
  25. package/dist/runtime/env-seeder.js.map +1 -1
  26. package/dist/runtime/launch.d.ts.map +1 -1
  27. package/dist/runtime/launch.js +0 -18
  28. package/dist/runtime/launch.js.map +1 -1
  29. package/edition/README.md +20 -3
  30. package/edition/biome-content.mjs +5 -2
  31. package/edition/build-runtime-image-source-index.mjs +79 -5
  32. package/edition/canonical-evidence-json.mjs +82 -0
  33. package/edition/catalogue-evidence.mjs +8 -5
  34. package/edition/customer-edition-verbs.mjs +9 -3
  35. package/edition/develop-coverage-run.mjs +2 -1
  36. package/edition/develop-coverage.mjs +12 -1
  37. package/edition/develop-edition-resolve.mjs +3 -2
  38. package/edition/develop-edition.mjs +15 -3
  39. package/edition/develop-record-verify.mjs +216 -0
  40. package/edition/develop-select-records.mjs +24 -225
  41. package/edition/develop-verify-image-record.mjs +90 -0
  42. package/edition/dist-check.mjs +24 -3
  43. package/edition/distribution-evidence-set.mjs +30 -2
  44. package/edition/edition-artifact.mjs +110 -22
  45. package/edition/edition-ceiling.mjs +180 -0
  46. package/edition/edition-config.mjs +91 -0
  47. package/edition/edition-constants.mjs +21 -3
  48. package/edition/edition-contract-parse.mjs +88 -3
  49. package/edition/edition-contract.json +47 -4
  50. package/edition/edition-image-seal.mjs +9 -2
  51. package/edition/edition-kind.mjs +10 -0
  52. package/edition/edition-protocol.mjs +7 -1
  53. package/edition/edition-publisher.mjs +21 -0
  54. package/edition/edition-seal.mjs +257 -0
  55. package/edition/evidence-grammars.mjs +2 -0
  56. package/edition/evidence-trust-pin.mjs +66 -10
  57. package/edition/evidence-trust-policy.mjs +39 -29
  58. package/edition/external-source-walk.mjs +16 -3
  59. package/edition/fleet-coordinate-finality.mjs +18 -5
  60. package/edition/fulcio-certificate-extensions.mjs +231 -0
  61. package/edition/image-evidence.mjs +34 -79
  62. package/edition/locked-bundle-signer.mjs +241 -0
  63. package/edition/oci-evidence-verbs.mjs +7 -1
  64. package/edition/pinned-cosign.mjs +70 -0
  65. package/edition/plan-image-evidence-refresh.mjs +3 -1
  66. package/edition/producer-evidence-bundle.mjs +3 -1
  67. package/edition/producer-evidence-index-file.mjs +4 -0
  68. package/edition/release-edition-artifact.mjs +3 -3
  69. package/edition/repin-evidence-trust.mjs +6 -1
  70. package/edition/resolve-distribution.mjs +90 -7
  71. package/edition/resolve-edition.mjs +1 -1
  72. package/edition/resolve-images.mjs +53 -7
  73. package/edition/seal-image-digests.mjs +19 -8
  74. package/edition/sealed-distribution-lock.mjs +2 -2
  75. package/edition/third-party-admission.mjs +91 -0
  76. package/edition/third-party-bundle-registry.mjs +147 -0
  77. package/edition/third-party-certificate-extensions.mjs +21 -0
  78. package/edition/third-party-image-pins.mjs +41 -0
  79. package/edition/third-party-pins-trusted.mjs +24 -0
  80. package/edition/third-party-producer-evidence.mjs +76 -0
  81. package/edition/third-party-source-check.mjs +28 -0
  82. package/edition/third-party-source-context.mjs +41 -0
  83. package/edition/third-party-source-evidence.mjs +255 -0
  84. package/edition/third-party-source-flags.mjs +134 -0
  85. package/edition/third-party-source-pins.mjs +259 -0
  86. package/edition/third-party-source-uniqueness.mjs +54 -0
  87. package/edition/third-party-source-walk.mjs +115 -0
  88. package/edition/third-party-source-writer.mjs +229 -0
  89. package/edition/third-party-subjects.mjs +128 -0
  90. package/edition/trust-policy-image-ownership.mjs +85 -0
  91. package/edition/verify-catalogue-evidence.mjs +2 -0
  92. package/edition/verify-image-evidence.mjs +2 -0
  93. package/edition/verify-image-platforms.mjs +33 -9
  94. package/package.json +5 -5
  95. package/dist/runtime/auth/client-bootstrap.d.ts +0 -11
  96. package/dist/runtime/auth/client-bootstrap.d.ts.map +0 -1
  97. package/dist/runtime/auth/client-bootstrap.js +0 -60
  98. package/dist/runtime/auth/client-bootstrap.js.map +0 -1
@@ -78,7 +78,7 @@ import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-compone
78
78
  import { fleetChartSetLockPath, writeEditionChartSet } from './edition-chart-set.mjs';
79
79
  import { workspaceSection } from './edition-config.mjs';
80
80
  import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
81
- import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, kernelPackageDir } from './edition-workspace.mjs';
81
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, dataRoot, editionsRoot, isEntryModule, kernelPackageDir } from './edition-workspace.mjs';
82
82
  import { walkDeclaredSources } from './external-source-walk.mjs';
83
83
  import {
84
84
  biomeTierOrder,
@@ -95,6 +95,7 @@ import {
95
95
  validateImmutableDistributionLock,
96
96
  validateServiceDependencyOwnerParity,
97
97
  } from './lib.mjs';
98
+ import { BundleSignerPolicyAbsentError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
98
99
  import {
99
100
  assertInstalledOperationCeilingAdmitted,
100
101
  assertOperationGrantsAdmitted,
@@ -112,6 +113,7 @@ import { commandFor } from './remedy-command.mjs';
112
113
  import { soleResourceProvider } from './required-resource-provider.mjs';
113
114
  import { readStorePublisherTrust } from './store-publisher-trust.mjs';
114
115
  import { readStrictJsonFile } from './strict-json.mjs';
116
+ import { isThirdPartyOciEntry } from './third-party-subjects.mjs';
115
117
  import { RuntimeSubjectKind } from './workspace-sections.mjs';
116
118
 
117
119
  /**
@@ -254,7 +256,14 @@ export function loadEditionManifest(parseDistribution, id) {
254
256
  if (!existsSync(path)) {
255
257
  throw new Error(`distribution "${id}" not found: ${path} does not exist`);
256
258
  }
257
- const raw = readStrictJsonFile(path);
259
+ return parseEditionManifest(parseDistribution, id, readStrictJsonFile(path), path);
260
+ }
261
+
262
+ /**
263
+ * The edition model's checks on one raw definition read from `source` (a file path, or a Git object): the refused
264
+ * fields, the kernel's validation and the id match. The one parse of a definition, whoever read the bytes.
265
+ */
266
+ export function parseEditionManifest(parseDistribution, id, raw, source) {
258
267
  if (raw !== null && typeof raw === 'object' && Object.hasOwn(raw, REFUSED_EXTENDS_FIELD)) {
259
268
  throw new EditionModelError(
260
269
  `edition "${id}": "${REFUSED_EXTENDS_FIELD}" is not part of the edition model; ` +
@@ -271,7 +280,7 @@ export function loadEditionManifest(parseDistribution, id) {
271
280
  // `sourcePins` reaches the kernel unchanged: its Distribution schema validates it.
272
281
  const manifest = parseDistribution(raw);
273
282
  if (manifest.id !== id) {
274
- throw new Error(`distribution at ${path} declares id "${manifest.id}", but it was addressed as "${id}"`);
283
+ throw new Error(`distribution at ${source} declares id "${manifest.id}", but it was addressed as "${id}"`);
275
284
  }
276
285
  return manifest;
277
286
  }
@@ -618,6 +627,11 @@ export function mergeExternalSourceEntries(
618
627
  * emitting `source: undefined` would make it read as first-party — so this
619
628
  * fails rather than choosing either.
620
629
  */
630
+ /** A stamped entry of a declared OCI source: third-party, an OCI bundle source and no git record. */
631
+ function isThirdPartyOciSourceEntry(entry, sourceRecords) {
632
+ return !sourceRecords.has(entry.source) && isThirdPartyOciEntry(entry);
633
+ }
634
+
621
635
  function requireSourceRecord(entry, sourceRecords) {
622
636
  const record = sourceRecords.get(entry.source);
623
637
  if (record === undefined) {
@@ -1780,6 +1794,41 @@ export function resolveDistributionFromInputs(
1780
1794
  // shipped provider. See `nexusBinder`.
1781
1795
  const nexusBindings = nexusBinder(id, [...selected.values()], schemas);
1782
1796
 
1797
+ // (5c') Bundle signers — every OCI bundle row (a biome's, a host module's) states the signer the data root's
1798
+ // trust policy selects for it. See `locked-bundle-signer.mjs`.
1799
+ const bundleSignerOf = (e, bundleSource, componentKey) => {
1800
+ const subject = { biomeId: e.id, ...(componentKey === undefined ? {} : { componentKey }), origin: e.origin, repo: e.repo, bundleSource };
1801
+ if (composed?.bundleSigners === undefined) {
1802
+ throw new BundleSignerPolicyAbsentError(
1803
+ `${componentKey === undefined ? `biome "${e.id}"` : `host module "${componentKey}" of biome "${e.id}"`} is an OCI bundle, but this resolve was given no bundle signer selection`,
1804
+ );
1805
+ }
1806
+ return composed.bundleSigners.signerFor(subject);
1807
+ };
1808
+ // A row drawn from a GIT source carries no OCI bundle (biome-level; an OCI host-module bundle is refused below), hence no bundleSigner: it is built
1809
+ // from the pinned source, and the manifest's `bundleSource` names a release artifact not built from this commit.
1810
+ // Omitted here, at the copy point. (An entry of a declared `oci` source has a stamp but no record: not git.)
1811
+ const isGitSourced = (e) => e.source !== undefined && sourceRecords.has(e.source);
1812
+ const bundleSourceOf = (e) => (e.bundleSource === undefined || isGitSourced(e) ? {} : { bundleSource: e.bundleSource });
1813
+ const bundleSignerFor = (e) => (e.bundleSource?.kind === 'oci' && !isGitSourced(e) ? { bundleSigner: bundleSignerOf(e, e.bundleSource) } : {});
1814
+ const hostModuleOf = (e, hostModule) => {
1815
+ if (isGitSourced(e)) {
1816
+ // The lock schema requires a host module's bundleSource, so an OCI one cannot be omitted: it is refused, in every
1817
+ // channel. A non-OCI one (npm, ...) is untouched.
1818
+ if (hostModule.bundleSource?.kind === 'oci') {
1819
+ throw new GitSourcedHostModuleBundleError(
1820
+ `host module "${hostModule.componentKey}" of biome "${e.id}" (git source "${e.source}") declares an OCI bundleSource (${hostModule.bundleSource.ociRef}); ` +
1821
+ 'such a host module bundle reaches an edition only through an `oci` source with a third-party source pin',
1822
+ );
1823
+ }
1824
+ return hostModule;
1825
+ }
1826
+ return hostModule.bundleSource?.kind === 'oci'
1827
+ ? { ...hostModule, bundleSigner: bundleSignerOf(e, hostModule.bundleSource, hostModule.componentKey) }
1828
+ : hostModule;
1829
+ };
1830
+ const hostModulesOf = (e) => (Array.isArray(e.hostModules) ? e.hostModules : []).map((hostModule) => hostModuleOf(e, hostModule));
1831
+
1783
1832
  // (5d) Dispatched runtime components — every `runtime-image` edge of a
1784
1833
  // shipped biome must resolve to a runtime component of a biome in the index.
1785
1834
  // The target biome is not rostered (it enters no `biomes[]`, `requires` or
@@ -1950,7 +1999,7 @@ export function resolveDistributionFromInputs(
1950
1999
  // (the connector adapters loaded by `biome-adapter-host`). Projected
1951
2000
  // verbatim from the index, which resolved each component's bundle source
1952
2001
  // against the biome manifest.
1953
- hostModules: Array.isArray(e.hostModules) ? e.hostModules : [],
2002
+ hostModules: hostModulesOf(e),
1954
2003
  // The BIOME-LEVEL bundle source, for a biome whose bundle no host module
1955
2004
  // above names — the field a `installPolicy: required` service-only
1956
2005
  // biome needs for the runtime's required-biome reconciler to fetch,
@@ -1963,7 +2012,8 @@ export function resolveDistributionFromInputs(
1963
2012
  // unconditional key would move the bytes of every committed lock for a
1964
2013
  // fact none of their entries carries. A lock in which SOME biome carries
1965
2014
  // it still is the one v5 grammar once sealed.
1966
- ...(e.bundleSource === undefined ? {} : { bundleSource: e.bundleSource }),
2015
+ ...bundleSourceOf(e),
2016
+ ...bundleSignerFor(e),
1967
2017
  serviceDependencies: Array.isArray(e.serviceDependencies)
1968
2018
  ? e.serviceDependencies
1969
2019
  : [],
@@ -2125,7 +2175,11 @@ export function resolveDistributionFromInputs(
2125
2175
  // scopes on — and the lock records the full pinned `LockedBiomeSource`,
2126
2176
  // so a reader of the artifact never has to resolve a handle against a
2127
2177
  // manifest it does not have.
2128
- ...(e.source === undefined
2178
+ //
2179
+ // A third-party OCI biome is stamped too (the edition fence keys on the stamp) but is NOT in `sourceRecords`:
2180
+ // the lock's `LockedBiomeSource` is git-only, so such a row carries no `source` and is held to its pin instead
2181
+ // (`third-party-subjects.mjs`).
2182
+ ...(e.source === undefined || isThirdPartyOciSourceEntry(e, sourceRecords)
2129
2183
  ? {}
2130
2184
  : { source: requireSourceRecord(e, sourceRecords) }),
2131
2185
  }))
@@ -2353,13 +2407,41 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
2353
2407
  id,
2354
2408
  index,
2355
2409
  schemas,
2356
- { ...composed, sourceRecords, storePublisherTrust },
2410
+ {
2411
+ ...composed,
2412
+ sourceRecords,
2413
+ storePublisherTrust,
2414
+ bundleSigners: inputs?.bundleSigners ?? loadBundleSignerSelection({ dataRoot: dataRoot(), schemas }),
2415
+ },
2357
2416
  new Date().toISOString(),
2358
2417
  platformServiceIndex,
2359
2418
  );
2360
2419
  return { lock, index };
2361
2420
  }
2362
2421
 
2422
+ /**
2423
+ * A release carries a third-party biome only as an `oci` source with a third-party pin. A git-sourced third-party biome
2424
+ * whose manifest declares an OCI bundleSource (biome-level or host module) names a main-release artifact that is not
2425
+ * built from the pinned commit, so a release resolve refuses it. `lock` is the resolved lock (a git row carries
2426
+ * `source`), `index` the index it was resolved from (which still holds the manifest's bundleSource).
2427
+ */
2428
+ export function assertReleaseGitSourcesDeclareNoBundle(lock, index) {
2429
+ const byId = new Map(index.map((entry) => [entry.id, entry]));
2430
+ for (const biome of lock.biomes) {
2431
+ if (biome.source === undefined) {continue;}
2432
+ const entry = byId.get(biome.id);
2433
+ const declared = [entry?.bundleSource, ...(entry?.hostModules ?? []).map((hostModule) => hostModule.bundleSource)].find(
2434
+ (bundleSource) => bundleSource?.kind === 'oci',
2435
+ );
2436
+ if (declared !== undefined) {
2437
+ throw new Error(
2438
+ `biome "${biome.id}" is drawn from git source "${biome.source.name}" and declares an OCI bundleSource (${declared.ociRef}); ` +
2439
+ 'a release does not carry it: declare the biome as an `oci` source with a third-party source pin instead',
2440
+ );
2441
+ }
2442
+ }
2443
+ }
2444
+
2363
2445
  /**
2364
2446
  * Resolve and persist one distribution lock. The CLI and the plane refresh use
2365
2447
  * this exact writer so an in-process refresh cannot drift from the standalone
@@ -2369,6 +2451,7 @@ export async function writeResolvedDistribution(id, { log = true, materialize, f
2369
2451
  const { lock, index } = await resolveDistributionWithIndex(id, { materialize });
2370
2452
  await assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest });
2371
2453
  const outPath = join(editionsRoot(), id, 'distribution.lock.json');
2454
+ assertReleaseGitSourcesDeclareNoBundle(lock, index);
2372
2455
  assertReleaseContent(lock, `the distribution lock for "${id}"`);
2373
2456
  assertReleaseEditionTree(join(editionsRoot(), id));
2374
2457
  mkdirSync(join(editionsRoot(), id), { recursive: true });
@@ -7,7 +7,7 @@
7
7
  // node resolve-edition.mjs <root flags> <edition-id> --trusted-write-v3
8
8
  // write the whole lock set into the editions root. The flag is the
9
9
  // operator's explicit acknowledgement that the images lock is rewritten
10
- // UNSEALED (a resolve never produces an `imageDigest`; `xema edition seal`
10
+ // UNSEALED (a resolve never produces an `imageDigest`; `xema edition resolve-digests`
11
11
  // writes them afterwards). Without it nothing is written.
12
12
  // node resolve-edition.mjs <root flags> <edition-id> --check
13
13
  // resolve the whole lock set into a scratch copy of the editions root and
@@ -72,6 +72,14 @@ import {
72
72
  } from './lib.mjs';
73
73
  import { assertReleaseContent } from './release-channel-guard.mjs';
74
74
  import { composeEdition } from './resolve-distribution.mjs';
75
+ import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
76
+ import {
77
+ assertOciBundleSourceOrigin,
78
+ attestedContentHash,
79
+ isThirdPartyOciRow,
80
+ thirdPartyPinForRow,
81
+ thirdPartySubjectCandidates,
82
+ } from './third-party-subjects.mjs';
75
83
 
76
84
  if (isEntryModule(import.meta.url)) {
77
85
  configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache]);
@@ -418,7 +426,7 @@ export function resolveImageStructure(
418
426
  enums,
419
427
  requiredPlatforms,
420
428
  declaredSources,
421
- { biomeIndex, supplementalSources } = {},
429
+ { biomeIndex, supplementalSources, thirdPartyPins } = {},
422
430
  ) {
423
431
  if (!Array.isArray(requiredPlatforms) || requiredPlatforms.length === 0) {
424
432
  throw new Error(
@@ -440,6 +448,9 @@ export function resolveImageStructure(
440
448
  );
441
449
  }
442
450
  const subjects = new Map();
451
+ // Third-party OCI subjects come from their pin and are never built, hashed or read from a checkout, so they are kept
452
+ // apart from every git-backed subject (see `third-party-subjects.mjs`).
453
+ const thirdPartySubjects = new Map();
443
454
  const webBiomes = [];
444
455
  const platformServiceBindings = [];
445
456
  const repositoryPins = new Map(
@@ -479,6 +490,15 @@ export function resolveImageStructure(
479
490
  };
480
491
 
481
492
  for (const biome of lock.biomes) {
493
+ const biomeLabel = `distribution "${lock.distributionId}" biome "${biome.id}"`;
494
+ assertOciBundleSourceOrigin(biome, biomeLabel);
495
+ if (isThirdPartyOciRow(biome)) {
496
+ const pin = thirdPartyPinForRow({ row: biome, pins: thirdPartyPins, declaredSources, label: biomeLabel });
497
+ for (const candidate of thirdPartySubjectCandidates({ row: biome, pin, enums, requiredPlatforms, label: biomeLabel })) {
498
+ addSubject(thirdPartySubjects, candidate);
499
+ }
500
+ continue;
501
+ }
482
502
  // WHERE THIS BIOME'S IMAGES LIVE, and WHICH COMMIT THEY ARE BUILT FROM.
483
503
  // Both answers move together when a biome is drawn from an external source,
484
504
  // because both are facts about that source rather than about this fleet.
@@ -662,7 +682,7 @@ export function resolveImageStructure(
662
682
  // distribution resolve if it dispatches, and the first-party index carries
663
683
  // none, so it is not looked up here.
664
684
  const dispatchers = lock.biomes
665
- .filter((biome) => biome.source === undefined && biome.target === 'server')
685
+ .filter((biome) => biome.source === undefined && !isThirdPartyOciRow(biome) && biome.target === 'server')
666
686
  .map((biome) => {
667
687
  const entries = biomeIndex.filter(
668
688
  (entry) =>
@@ -892,13 +912,29 @@ export function resolveImageStructure(
892
912
  platformServiceBindings.sort((left, right) =>
893
913
  compareStrings(left.platformService, right.platformService),
894
914
  );
915
+ const orderedThirdPartySubjects = [...thirdPartySubjects.values()]
916
+ .map((subject) => ({ ...subject, owners: subject.owners.sort(compareOwner) }))
917
+ .sort(compareSubject);
918
+ for (const subject of orderedThirdPartySubjects) {
919
+ if (subjects.has(subjectKey(subject))) {
920
+ throw new Error(
921
+ `third-party runtime image subject "${subject.kind}:${subject.id}" has the identity of another subject of distribution "${lock.distributionId}"`,
922
+ );
923
+ }
924
+ }
895
925
  return {
896
926
  subjects: orderedSubjects,
927
+ thirdPartySubjects: orderedThirdPartySubjects,
897
928
  webBiomes,
898
929
  platformServiceBindings,
899
930
  };
900
931
  }
901
932
 
933
+ /** Every image subject of a structure, first-party and third-party, in the lock's order. */
934
+ export function structureSubjects(structure) {
935
+ return [...structure.subjects, ...structure.thirdPartySubjects].sort(compareSubject);
936
+ }
937
+
902
938
  /**
903
939
  * THE COMPOSED-PROCESS IMAGE OF ONE BUILD — where a producing repository
904
940
  * declares it, and how an edition addresses it.
@@ -1775,7 +1811,11 @@ export async function resolveImageRows(id, { inputs, lock, onSubjectHashed } = {
1775
1811
  schemas,
1776
1812
  requiredPlatforms,
1777
1813
  declaredSources,
1778
- { biomeIndex, supplementalSources: supplementalSourcesFor(runtimeSourceIndex) },
1814
+ {
1815
+ biomeIndex,
1816
+ supplementalSources: supplementalSourcesFor(runtimeSourceIndex),
1817
+ thirdPartyPins: loadThirdPartySourcePins(dataRoot()),
1818
+ },
1779
1819
  ),
1780
1820
  schemas,
1781
1821
  requiredPlatforms,
@@ -1909,8 +1949,11 @@ export async function resolveImageRows(id, { inputs, lock, onSubjectHashed } = {
1909
1949
  'refusing image-lock write: a source repository commit changed while hashing',
1910
1950
  );
1911
1951
  }
1952
+ // After the ledger check and the hashing: a third-party subject has no ledger row and no checkout, and its
1953
+ // `contentHash` is the publisher's statement, so none of the above read it.
1912
1954
  return {
1913
- subjects,
1955
+ subjects: structure.thirdPartySubjects.length === 0 ? subjects : [...subjects, ...structure.thirdPartySubjects].sort(compareSubject),
1956
+ thirdPartySubjects: structure.thirdPartySubjects,
1914
1957
  platformServiceBindings: structure.platformServiceBindings,
1915
1958
  webBiomes,
1916
1959
  contractAuthority: runtimeSourceIndex.contractAuthority,
@@ -1920,7 +1963,7 @@ export async function resolveImageRows(id, { inputs, lock, onSubjectHashed } = {
1920
1963
 
1921
1964
  /** One edition's COMPLETE image lock: its rows, parsed and validated as the kernel's image lock. */
1922
1965
  export async function resolveImages(id, { inputs, lock, onSubjectHashed } = {}) {
1923
- const { subjects, platformServiceBindings, webBiomes, contractAuthority, commits } = await resolveImageRows(id, { inputs, lock, onSubjectHashed });
1966
+ const { subjects, thirdPartySubjects, platformServiceBindings, webBiomes, contractAuthority, commits } = await resolveImageRows(id, { inputs, lock, onSubjectHashed });
1924
1967
  const schemas = await loadKernelDistributionSchemas();
1925
1968
  const imageLock = validateImmutableImageLock(
1926
1969
  schemas.parseImageLock({
@@ -1934,7 +1977,7 @@ export async function resolveImages(id, { inputs, lock, onSubjectHashed } = {})
1934
1977
  }),
1935
1978
  schemas,
1936
1979
  );
1937
- return { imageLock, commits };
1980
+ return { imageLock, commits, thirdPartySubjects };
1938
1981
  }
1939
1982
 
1940
1983
  /**
@@ -1954,7 +1997,7 @@ export async function writeResolvedImages(
1954
1997
  }
1955
1998
  // The RELEASE lock is published-only: a develop edition resolves through
1956
1999
  // `develop-edition.mjs` and never reaches this writer.
1957
- const { imageLock, commits } = await resolveImages(id);
2000
+ const { imageLock, commits, thirdPartySubjects } = await resolveImages(id);
1958
2001
  const outPath = distributionOutputPath(
1959
2002
  editionsRoot(),
1960
2003
  id,
@@ -1969,6 +2012,9 @@ export async function writeResolvedImages(
1969
2012
  for (const [repo, commit] of commits) {
1970
2013
  console.log(` source ${repo}@${commit}`);
1971
2014
  }
2015
+ for (const subject of thirdPartySubjects) {
2016
+ console.log(` third-party ${subject.kind}:${subject.id} ${subject.imageRef} ${attestedContentHash(subject)}`);
2017
+ }
1972
2018
  }
1973
2019
  return { imageLock, commits };
1974
2020
  }
@@ -121,7 +121,7 @@ import { parseCustomerDefinition } from './customer-definition.mjs';
121
121
  import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG } from './customer-flags.mjs';
122
122
  import { EditionChannel } from './edition-constants.mjs';
123
123
  import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
124
- import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
124
+ import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, dataRoot, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
125
125
  import {
126
126
  assertCoordinatesFinal,
127
127
  collectFleetPlatformRequirements,
@@ -137,6 +137,8 @@ import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
137
137
  import { readSealedDistributionLock } from './sealed-distribution-lock.mjs';
138
138
  import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
139
139
  import { readStrictJsonFile } from './strict-json.mjs';
140
+ import { pinnedManifestDigests, thirdPartyImagePins } from './third-party-image-pins.mjs';
141
+ import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
140
142
  import {
141
143
  loadImageLockForVerification,
142
144
  verifyDistributionPlatforms,
@@ -569,14 +571,14 @@ export async function sealEditionLocks({ distributionId, root, digests = new Map
569
571
  * The decision, with no files: an UNSEALED lock is sealed (refused under `check`); a SEALED lock is VERIFIED -
570
572
  * every hash recomputed, a mismatch refused naming the biome.
571
573
  */
572
- export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, check = false, lockPath = 'distribution.lock.json' }) {
574
+ export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners, check = false, lockPath = 'distribution.lock.json' }) {
573
575
  if ('unsealedSchema' in lock) {
574
576
  if (check) {
575
- throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition seal ${lock.distributionId}\``);
577
+ throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition resolve-digests ${lock.distributionId}\``);
576
578
  }
577
- return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) };
579
+ return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners }) };
578
580
  }
579
- await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys });
581
+ await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners });
580
582
  return { sealed: false, lock };
581
583
  }
582
584
 
@@ -586,7 +588,7 @@ async function main() {
586
588
  parseSealArgs(args);
587
589
  if (!distributionId || rest.length > 0) {
588
590
  throw new Error(
589
- 'usage: xema edition seal <distributionId> ' +
591
+ 'usage: xema edition resolve-digests <distributionId> ' +
590
592
  `[--check | --if-published | --publication-verdict] ` +
591
593
  `[--definition <file> [--catalogue-username <name> --catalogue-token-env <VAR>]]`,
592
594
  );
@@ -700,12 +702,16 @@ async function main() {
700
702
  ? undefined
701
703
  : parseCustomerDefinition(await adoptCatalogueKernel(catalogue), readStrictJsonFile(catalogueFlags.get(DEFINITION_FLAG))).definition.storePublisherAnchorKeys;
702
704
  const imageLock = await loadImageLockForVerification(distributionId, root);
705
+ // A pinned third-party subject (its repository is in the data root's third-party source pins) is verified and sealed
706
+ // BY the pin's manifest digest: its contentHash is publisher-attested and is not a tag.
707
+ const thirdPartyPins = loadTrustedThirdPartySourcePins(dataRoot());
708
+ const pinnedImages = thirdPartyImagePins({ pins: thirdPartyPins, subjects: imageLock.subjects });
703
709
 
704
710
  // Throws on ANY unresolvable subject or missing required platform, before a
705
711
  // single byte of the lock is touched.
706
712
  let report;
707
713
  try {
708
- report = await verifyDistributionPlatforms({ imageLock, distributionsRoot: root });
714
+ report = await verifyDistributionPlatforms({ imageLock, distributionsRoot: root, pinnedDigests: pinnedManifestDigests(pinnedImages) });
709
715
  } catch (error) {
710
716
  // RECONCILE: a coordinate this pass just re-pinned has not been built yet.
711
717
  // Skip the EDITION — never some of its subjects — and leave the lock
@@ -839,14 +845,19 @@ async function main() {
839
845
  // introduced and the repository holding every edition's lock is where the
840
846
  // question is answerable at all.
841
847
  const { editions, withoutLock } = readFleetImageLocks(root);
842
- const fleetRequirements = collectFleetPlatformRequirements({ editions });
848
+ const fleetRequirements = collectFleetPlatformRequirements({ editions, isThirdPartySubject: (subject) => thirdPartyPins.byRepository(subject.repo) !== undefined });
843
849
  try {
844
850
  const finality = assertCoordinatesFinal({
845
851
  distributionId,
846
852
  writes,
847
853
  verified: report.subjects,
848
854
  fleetRequirements,
855
+ thirdPartyKeys: new Set(pinnedImages.keys()),
849
856
  });
857
+ // Third-party subjects are NOT graded by the first-party fleet verdict, and are named here rather than skipped silently.
858
+ for (const key of finality.thirdParty) {
859
+ console.log(`[${label}] finality: ${key} third-party (pinned by digest), not a first-party fleet coordinate.`);
860
+ }
850
861
  // The CORPUS, not a confirmation: how many coordinates were graded against
851
862
  // how many editions, and which editions made no claim. Without the
852
863
  // denominators a pass cannot be told from a pass over nothing, and the
@@ -1,5 +1,5 @@
1
1
  // sealed-distribution-lock — the one predicate for "this committed distribution.lock.json is the SEALED grammar".
2
- // `xema edition artifact` and the `xema edition seal --publication-verdict` gate both ask it, so the verdict can
2
+ // `xema edition artifact` and the `xema edition resolve-digests --publication-verdict` gate both ask it, so the verdict can
3
3
  // never say PUBLISHABLE for a lock the artifact verb then refuses.
4
4
 
5
5
  import { readFileSync } from 'node:fs';
@@ -15,7 +15,7 @@ export function readSealedDistributionLock({ schemas, lockPath, distributionId }
15
15
  } catch (error) {
16
16
  throw new Error(
17
17
  `${lockPath} is not a sealed distribution lock, so nothing is published from it. ` +
18
- `Run \`xema edition seal ${distributionId}\` first. (${error.message})`,
18
+ `Run \`xema edition resolve-digests ${distributionId}\` first. (${error.message})`,
19
19
  { cause: error },
20
20
  );
21
21
  }
@@ -0,0 +1,91 @@
1
+ // Admission of third-party image subjects (XEMA-1611 design section 4), beside `externalSourceRepositoryPins` and
2
+ // `verifyDistributionImageEvidenceSet` of `image-evidence.mjs`. Pure over data: the pins come from the data root's
3
+ // `third-party-source-pins.json` (held to the evidence trust pin by the entry), the statement and bundle from a
4
+ // descriptor `verifySigstoreEvidenceDescriptor` has just verified.
5
+
6
+ import { assertCertificateExtensions } from './third-party-certificate-extensions.mjs';
7
+
8
+ /** The declared source kind that names a third-party pin. */
9
+ const OCI_SOURCE_KIND = 'oci';
10
+ const FIRST_PARTY_LAYOUT_PREFIX = /^(?:repos|submodules)\//u;
11
+
12
+ const repositoryPathOf = (repositoryUrl) => new URL(repositoryUrl).pathname.replace(/^\//u, '').replace(/\.git$/u, '');
13
+
14
+ /**
15
+ * The repository pins of the edition's declared OCI sources, derived from their third-party source pins:
16
+ * `repo` (the pinned `owner/name`), `repositoryUrl` and `sourceCommit`. Every declared OCI source must be pinned (a
17
+ * missing or empty pin file refuses, with the loader's reason), and the pin must not collide with an index pin or a git
18
+ * source, nor sit under the first-party layout (`repos/`, `submodules/`). A declared source that ships no image yields no
19
+ * pin; every subject of a source must agree on the pinned commit.
20
+ *
21
+ * @returns {Map<string, {repo: string, repositoryUrl: string, sourceCommit: string}>}
22
+ */
23
+ export function thirdPartySourceRepositoryPins({ distribution, imageLock, repositoryPins, thirdPartyPins }) {
24
+ const sources = distribution.sources ?? [];
25
+ const derived = new Map();
26
+ for (const source of sources) {
27
+ if (source.kind !== OCI_SOURCE_KIND) {continue;}
28
+ const pin = thirdPartyPins.byName(source.name);
29
+ if (pin === undefined) {
30
+ throw new Error(
31
+ `edition source "${source.name}" is kind "${OCI_SOURCE_KIND}", but ${thirdPartyPins.reason ?? `the data root's third-party source pins hold no pin named "${source.name}"`}`,
32
+ );
33
+ }
34
+ if (FIRST_PARTY_LAYOUT_PREFIX.test(pin.repository)) {
35
+ throw new Error(`third-party source "${pin.name}" pins repository "${pin.repository}", which is under the first-party layout (repos/ or submodules/)`);
36
+ }
37
+ if (repositoryPins.has(pin.repository)) {
38
+ throw new Error(`third-party source "${pin.name}" repository "${pin.repository}" shadows a repository pin the runtime image source index already commits`);
39
+ }
40
+ const shadowed = sources.find((candidate) => candidate.kind === 'git' && (candidate.name === pin.repository || candidate.name === pin.name));
41
+ if (shadowed !== undefined) {
42
+ throw new Error(`third-party source "${pin.name}" (${pin.repository}) collides with the git source "${shadowed.name}" of the same edition`);
43
+ }
44
+ const subjects = imageLock.subjects.filter((subject) => subject.repo === pin.repository);
45
+ if (subjects.length === 0) {continue;}
46
+ const wrong = subjects.filter((subject) => subject.sourceCommit !== pin.sourceCommit);
47
+ if (wrong.length > 0) {
48
+ throw new Error(
49
+ `third-party source "${pin.name}" is pinned at ${pin.sourceCommit}, but images.lock.json builds ${wrong.map((subject) => `${subject.kind}:${subject.id}`).join(', ')} at ${[...new Set(wrong.map((subject) => subject.sourceCommit))].join(', ')}`,
50
+ );
51
+ }
52
+ derived.set(pin.repository, { repo: pin.repository, repositoryUrl: pin.repositoryUrl, sourceCommit: pin.sourceCommit });
53
+ }
54
+ return derived;
55
+ }
56
+
57
+ /** The pins behind `thirdPartySourceRepositoryPins`, by repository. */
58
+ export const thirdPartySourcesByRepository = (derived, thirdPartyPins) =>
59
+ new Map([...derived.keys()].map((repo) => [repo, thirdPartyPins.byRepository(repo)]));
60
+
61
+ /**
62
+ * What a third-party subject must satisfy beyond a first-party one, after its evidence has verified:
63
+ * * [d9-1] the signing row's workflow repository, the subject's `repo` and the path of the predicate's
64
+ * `source.repositoryUrl` are one repository;
65
+ * * the subject is the pinned image (reference, content hash, sealed digest when sealed);
66
+ * * (d) the leaf certificate states the pinned repository id, owner id and a GitHub-hosted runner.
67
+ */
68
+ export function assertThirdPartySubjectAdmission({ subject, source, statement, identityPolicy, bundle }) {
69
+ const label = `third-party subject "${subject.kind}:${subject.id}" (source "${source.name}")`;
70
+ const claimedRepository = repositoryPathOf(statement.predicate.source.repositoryUrl);
71
+ const signerRepository = identityPolicy.githubWorkflow.repository;
72
+ if (signerRepository !== subject.repo || claimedRepository !== subject.repo) {
73
+ throw new Error(
74
+ `${label}: the signing row's workflow repository (${signerRepository}), the subject repo (${subject.repo}) and the predicate source.repositoryUrl repository (${claimedRepository}) must be one repository`,
75
+ );
76
+ }
77
+ const image = source.images.find((candidate) => candidate.subjectKind === subject.kind && candidate.subjectId === subject.id);
78
+ if (image === undefined) {
79
+ throw new Error(`${label}: the pin pins no image for it`);
80
+ }
81
+ if (subject.imageRef !== image.imageRef) {
82
+ throw new Error(`${label}: images.lock.json names ${subject.imageRef}, but the pin pins ${image.imageRef}`);
83
+ }
84
+ if (subject.contentHash !== image.sourceTreeHash) {
85
+ throw new Error(`${label}: images.lock.json has contentHash ${subject.contentHash}, but the pin records sourceTreeHash ${image.sourceTreeHash}`);
86
+ }
87
+ if (subject.imageDigest !== undefined && subject.imageDigest !== image.manifestDigest) {
88
+ throw new Error(`${label}: images.lock.json seals ${subject.imageDigest}, but the pin records manifestDigest ${image.manifestDigest}`);
89
+ }
90
+ assertCertificateExtensions({ bundle, source, label });
91
+ }