@xemahq/xema 0.48.0 → 0.49.0

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 +22 -2
  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 +224 -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 +47 -6
  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 +127 -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
@@ -0,0 +1,127 @@
1
+ // Third-party OCI biomes on the resolve side (XEMA-1611 design section 4).
2
+ //
3
+ // A biome from an organisation outside the vendor fleet reaches an edition as a lock row with NO `source` (the lock's
4
+ // `LockedBiomeSource` is git-only), an OCI `bundleSource` and `origin: third-party`. That triple is the whole key; the
5
+ // pin file (`third-party-source-pins.json`, written only by the trusted writer) is what the row is then held to. The
6
+ // image subjects of such a row take `imageRef`, `sourceCommit` and `contentHash` from the pin and are never built,
7
+ // hashed or probed by this plane: there is no checkout to hash.
8
+ //
9
+ // `contentHash` of such a subject is the PUBLISHER'S statement (`sourceTreeHash` of its image-source evidence), never
10
+ // a hash this plane computed. It is therefore labelled `contentHash (publisher-attested)` wherever it is printed and
11
+ // is not used as an image tag, a cache key or a reproducibility check by any path that serves third-party subjects.
12
+
13
+ import { loadKernelBiomeContracts } from './lib.mjs';
14
+
15
+ export const PUBLISHER_ATTESTED_LABEL = 'publisher-attested';
16
+
17
+ export const OCI_BUNDLE_SOURCE_KIND = 'oci';
18
+
19
+ const hasOciBundleSource = (row) => row.bundleSource?.kind === OCI_BUNDLE_SOURCE_KIND;
20
+ const isThirdPartyOrigin = (row) => row.origin === loadKernelBiomeContracts().BiomeOrigin.ThirdParty;
21
+
22
+ /**
23
+ * The lock key of a third-party OCI biome: no `source`, an OCI `bundleSource`, `origin: third-party`.
24
+ * A lock row and a walked index entry both carry these fields; an index entry's `source` is the walk's stamp, so it
25
+ * is dropped by the caller that tests an entry (`isThirdPartyOciEntry`).
26
+ */
27
+ export const isThirdPartyOciRow = (row) => row.source === undefined && hasOciBundleSource(row) && isThirdPartyOrigin(row);
28
+
29
+ /** A walked index entry that is a third-party OCI biome: its `source` is the walk's stamp, not a lock source. */
30
+ export const isThirdPartyOciEntry = (entry) => hasOciBundleSource(entry) && isThirdPartyOrigin(entry);
31
+
32
+ /**
33
+ * Refuse an OCI `bundleSource` on a row whose origin is not third-party: only a third-party biome is pulled from an OCI
34
+ * bundle by digest, so the vendor's origin on such a row would place an unevidenced bundle in the vendor's trust domain.
35
+ * A row drawn from a git `source` is held by the source walk and is not this key.
36
+ */
37
+ export function assertOciBundleSourceOrigin(row, label) {
38
+ if (row.source === undefined && hasOciBundleSource(row) && !isThirdPartyOrigin(row)) {
39
+ throw new Error(
40
+ `${label} carries an OCI bundleSource (${row.bundleSource.ociRef}) but its origin is "${row.origin}": an OCI ` +
41
+ `biome bundle is admitted only as origin "${loadKernelBiomeContracts().BiomeOrigin.ThirdParty}", with a pin of the data root's third-party source pins`,
42
+ );
43
+ }
44
+ }
45
+
46
+ /**
47
+ * The third-party source pin a third-party OCI row is held to, by the row's `repo` (the pinned repository). Refuses a
48
+ * row with no pin (naming why the data root has none when it has none), a pin for another biome, another bundle
49
+ * reference or another manifest hash, and a pin the distribution does not declare as an `oci` source of that name.
50
+ *
51
+ * @param {{row: object, declaredSources: readonly object[]|undefined, pins: {sources: readonly object[], reason: string|null, byRepository: (repository: string) => object|undefined}|undefined, label: string}} options
52
+ */
53
+ export function thirdPartyPinForRow({ row, pins, declaredSources, label }) {
54
+ if (pins === undefined) {
55
+ throw new Error(`${label} is a third-party OCI biome, but this resolve was given no third-party source pins to hold it to`);
56
+ }
57
+ const pin = pins.byRepository(row.repo);
58
+ if (pin === undefined) {
59
+ throw new Error(
60
+ `${label} is a third-party OCI biome with neither a source nor a pin: ${pins.reason ?? `no third-party source pin names repository "${row.repo}"`}`,
61
+ );
62
+ }
63
+ if (row.id !== pin.bundle.biomeId) {
64
+ throw new Error(`${label} has biome id "${row.id}", but the third-party pin "${pin.name}" is for biome "${pin.bundle.biomeId}"`);
65
+ }
66
+ if (row.bundleSource.ociRef !== pin.bundle.ociRef) {
67
+ throw new Error(`${label} names bundle ${row.bundleSource.ociRef}, but the third-party pin "${pin.name}" pins ${pin.bundle.ociRef}`);
68
+ }
69
+ if (row.sourceManifestHash !== pin.bundle.sourceManifestHash) {
70
+ throw new Error(
71
+ `${label} has sourceManifestHash ${row.sourceManifestHash}, but the third-party pin "${pin.name}" pins ${pin.bundle.sourceManifestHash}`,
72
+ );
73
+ }
74
+ if (!Array.isArray(declaredSources) || !declaredSources.some((source) => source.kind === OCI_BUNDLE_SOURCE_KIND && source.name === pin.name)) {
75
+ throw new Error(`${label} is drawn from the third-party pin "${pin.name}", which the distribution does not declare as a sources[] entry of kind "${OCI_BUNDLE_SOURCE_KIND}"`);
76
+ }
77
+ return pin;
78
+ }
79
+
80
+ /**
81
+ * The image subjects of one third-party OCI row, from its pin and nothing else: one per service and runtime
82
+ * component the row ships (a worker that runs its sibling's image has none). Every pinned image must be one of them and
83
+ * every shipped image must be pinned, so the pin and the lock describe the same images.
84
+ *
85
+ * `contentHash` is the publisher's `sourceTreeHash` (publisher-attested); `sourceCommit` is the pin's.
86
+ */
87
+ export function thirdPartySubjectCandidates({ row, pin, enums, requiredPlatforms, label }) {
88
+ if (row.target === 'web') {
89
+ throw new Error(`${label} is a third-party OCI biome with target "web": a bundled web biome of a third party is not admitted (it has no pinned image)`);
90
+ }
91
+ const sharedImageWorkers = new Set(Object.keys(row.workerImages ?? {}));
92
+ const wanted = [
93
+ ...(row.services ?? []).filter((service) => !sharedImageWorkers.has(service)).map((id) => [enums.RuntimeImageSubjectKind.Service, id]),
94
+ ...(row.runtimeComponents ?? []).map((id) => [enums.RuntimeImageSubjectKind.RuntimeComponent, id]),
95
+ ];
96
+ const byKey = new Map(pin.images.map((image) => [`${image.subjectKind}\0${image.subjectId}`, image]));
97
+ const used = new Set();
98
+ const candidates = wanted.map(([kind, id]) => {
99
+ const image = byKey.get(`${kind}\0${id}`);
100
+ if (image === undefined) {
101
+ throw new Error(`${label} ships ${kind} "${id}", which the third-party pin "${pin.name}" pins no image for`);
102
+ }
103
+ used.add(`${kind}\0${id}`);
104
+ return {
105
+ kind,
106
+ id,
107
+ imageRef: image.imageRef,
108
+ repo: row.repo,
109
+ path: row.path,
110
+ sourceCommit: pin.sourceCommit,
111
+ sourceManifestHash: row.sourceManifestHash,
112
+ requiredPlatforms,
113
+ owners: [{ kind: enums.RuntimeImageOwnerKind.Biome, biomeId: row.id }],
114
+ contentHash: image.sourceTreeHash,
115
+ };
116
+ });
117
+ const unused = [...byKey.keys()].filter((key) => !used.has(key));
118
+ if (unused.length > 0) {
119
+ throw new Error(
120
+ `the third-party pin "${pin.name}" pins image(s) ${unused.map((key) => key.replace('\0', ':')).join(', ')} that ${label} does not ship`,
121
+ );
122
+ }
123
+ return candidates;
124
+ }
125
+
126
+ /** A third-party subject's content hash, as every CLI output prints it. */
127
+ export const attestedContentHash = (subject) => `contentHash (${PUBLISHER_ATTESTED_LABEL}) ${subject.contentHash}`;
@@ -0,0 +1,85 @@
1
+ // Who may name which image repository in the evidence trust policy (XEMA-1611 design section 3(e)).
2
+ //
3
+ // Checked when the declared policy is LOADED, so every consumer of the policy sees it:
4
+ // (a) an `imageRepositories` entry listed by rows whose `githubWorkflow.repository` OWNERS differ is
5
+ // refused: one repository, one publisher;
6
+ // (b) a row whose owner is not a first-party owner and that names a first-party namespace
7
+ // (`<registry>/<first-party owner>/<any>`) is refused.
8
+ // First-party owners are the owners of the runtime-image-source index `repositoryPins` URLs, plus the kernel scope owner.
9
+
10
+ import { KERNEL_PACKAGE_NAME } from './kernel-package.mjs';
11
+
12
+ /**
13
+ * The vendor's publishing owner: the npm scope of the kernel package (`@<scope>/contracts`), read from the package name
14
+ * the CLI already pins, not written here. It is a first-party owner on purpose, beside the owners of the index's
15
+ * `repositoryPins`: the vendor publishes its kernel-scoped images under that owner, which no repository pin names, so
16
+ * without it a vendor row for them would be refused by rule (b) as a third party.
17
+ */
18
+ export const KERNEL_SCOPE_OWNER = KERNEL_PACKAGE_NAME.replace(/^@/u, '').split('/')[0];
19
+
20
+ /** The lowercase owners of the `repositoryPins[].repositoryUrl` entries of a parsed runtime image source index, plus the kernel scope owner. */
21
+ export function firstPartyOwnersFromIndex(indexDocument) {
22
+ const pins = indexDocument?.repositoryPins;
23
+ if (!Array.isArray(pins)) {
24
+ throw new Error('runtime image source index has no repositoryPins array to derive the first-party owners from');
25
+ }
26
+ const owners = new Set([KERNEL_SCOPE_OWNER]);
27
+ for (const pin of pins) {
28
+ let url;
29
+ try {
30
+ url = new URL(pin?.repositoryUrl);
31
+ } catch {
32
+ throw new Error(`runtime image source index repositoryPins entry ${JSON.stringify(pin?.repo)} has no parsable repositoryUrl`);
33
+ }
34
+ const owner = url.pathname.split('/').filter(Boolean)[0];
35
+ if (owner === undefined) {
36
+ throw new Error(`runtime image source index repositoryUrl ${url.href} names no owner`);
37
+ }
38
+ owners.add(owner.toLowerCase());
39
+ }
40
+ return owners;
41
+ }
42
+
43
+ /**
44
+ * Whether an image repository sits in a first-party namespace: `<registry>/<first-party owner>/<any>`, an owner being
45
+ * one of `firstPartyOwners` (the data root's own, from `firstPartyOwnersFromIndex`). The registry is whatever the first
46
+ * path segment is; no registry or organisation is named here.
47
+ */
48
+ export function isFirstPartyImageNamespace(imageRepository, firstPartyOwners) {
49
+ const segments = imageRepository.toLowerCase().split('/');
50
+ return segments.slice(1, -1).some((segment) => firstPartyOwners.has(segment));
51
+ }
52
+
53
+ const rowOwner = (row) => row.githubWorkflow?.repository?.split('/')[0]?.toLowerCase();
54
+
55
+ /**
56
+ * Refuse a policy that breaks (a) or (b). `firstPartyOwners` is `null` when the data root has no runtime image source
57
+ * index (a customer data root): (b) cannot be judged there and is not applied; (a) always is.
58
+ *
59
+ * @param {{policy: {policies: object[]}, firstPartyOwners: Set<string>|null}} options
60
+ */
61
+ export function assertTrustPolicyImageOwnership({ policy, firstPartyOwners }) {
62
+ const ownersByRepository = new Map();
63
+ for (const row of policy.policies) {
64
+ const owner = rowOwner(row);
65
+ if (owner === undefined || !Array.isArray(row.imageRepositories)) {
66
+ continue;
67
+ }
68
+ for (const imageRepository of row.imageRepositories) {
69
+ const seen = ownersByRepository.get(imageRepository);
70
+ if (seen !== undefined && seen.owner !== owner) {
71
+ throw new Error(
72
+ `evidence trust policy: image repository ${imageRepository} is listed by rows of two publishers ` +
73
+ `(${seen.policyId} owned by ${seen.owner}, ${row.policyId} owned by ${owner}); one image repository has one publisher`,
74
+ );
75
+ }
76
+ ownersByRepository.set(imageRepository, { owner, policyId: row.policyId });
77
+ if (firstPartyOwners !== null && !firstPartyOwners.has(owner) && isFirstPartyImageNamespace(imageRepository, firstPartyOwners)) {
78
+ throw new Error(
79
+ `evidence trust policy: row ${row.policyId} (owner ${owner}, not a first-party owner) names image repository ` +
80
+ `${imageRepository} in a first-party namespace (<registry>/<first-party owner>/<any>)`,
81
+ );
82
+ }
83
+ }
84
+ }
85
+ }
@@ -24,6 +24,7 @@ import { EDITION_CONTRACT } from './edition-constants.mjs';
24
24
  import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot } from './edition-workspace.mjs';
25
25
  import {
26
26
  assertPinnedPolicy,
27
+ assertPinnedThirdPartySourcePins,
27
28
  assertPinnedVerifier,
28
29
  loadDeclaredTrustPolicy,
29
30
  readEvidenceTrustPin,
@@ -143,6 +144,7 @@ async function armedAnchors({ channel, env, bundlePath }) {
143
144
  const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot: dataRoot(), declaration, schemas });
144
145
  const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
145
146
  assertPinnedPolicy({ pin, policyDigest });
147
+ assertPinnedThirdPartySourcePins({ pin, dataRoot: dataRoot() });
146
148
  assertPinnedVerifier({ pin, actual: verifierDigests(running) });
147
149
  const { state, unmet } = assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue: true });
148
150
  if (state !== TrustPolicyArmingState.ARMED) {
@@ -45,6 +45,7 @@ import {
45
45
  RUNTIME_IMAGE_SOURCE_INDEX_FILE,
46
46
  assertPinnedKernelContractAuthority,
47
47
  assertPinnedPolicy,
48
+ assertPinnedThirdPartySourcePins,
48
49
  assertPinnedVerifier,
49
50
  readStatedKernelContractAuthority,
50
51
  loadDeclaredTrustPolicy,
@@ -264,6 +265,7 @@ function readTrustAnchors({ schemas, running }) {
264
265
  });
265
266
  const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
266
267
  assertPinnedPolicy({ pin, policyDigest });
268
+ assertPinnedThirdPartySourcePins({ pin, dataRoot: dataRoot() });
267
269
  assertPinnedVerifier({ pin, actual: verifierDigests(running) });
268
270
  // The authority the producer statements are held to is read from the data
269
271
  // root's index downstream; it is the pinned one, or nothing is verified.
@@ -55,6 +55,7 @@ import {
55
55
  splitImageRef,
56
56
  } from './registry-credentials.mjs';
57
57
  import { assertReleaseContent, assertReleaseEditionTree } from './release-channel-guard.mjs';
58
+ import { imageSubjectKey } from './third-party-image-pins.mjs';
58
59
 
59
60
  if (isEntryModule(import.meta.url)) {
60
61
  configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Kernel]);
@@ -182,9 +183,15 @@ async function readManifestConfigPlatform(client, repository, manifest, label) {
182
183
  * Resolve the set of platforms a published coordinate ACTUALLY serves.
183
184
  * Every returned platform is proven by a hash-verified image config, never by
184
185
  * an index descriptor's self-declaration alone.
186
+ *
187
+ * The coordinate is read by exactly one of `tag` (a first-party `<contentHash>` tag) or `digest` (a third-party
188
+ * subject, read by its pinned manifest digest).
185
189
  */
186
- export async function resolvePublishedPlatforms({ client, repository, tag, coordinate }) {
187
- const top = await client.getManifestByTag(repository, tag);
190
+ export async function resolvePublishedPlatforms({ client, repository, tag, digest, coordinate }) {
191
+ if ((tag === undefined) === (digest === undefined)) {
192
+ throw new Error(`${coordinate} must be read by exactly one of a tag or a digest (got ${tag === undefined ? 'neither' : 'both'})`);
193
+ }
194
+ const top = tag === undefined ? await client.getManifestByDigest(repository, digest) : await client.getManifestByTag(repository, tag);
188
195
  const platforms = [];
189
196
 
190
197
  if (!top.isIndex) {
@@ -340,6 +347,9 @@ export async function verifyDistributionPlatforms({
340
347
  collectFailures = false,
341
348
  // `(line) => void`: one line per verified subject, with its elapsed ms; null is silent.
342
349
  progress = null,
350
+ // `kind:id` -> manifest digest of the subjects pinned by a third-party source pin: each is read BY that digest, never by
351
+ // its (publisher-attested) contentHash as a tag.
352
+ pinnedDigests = new Map(),
343
353
  }) {
344
354
  const declarations = loadRegistryCredentialDeclarations(distributionsRoot);
345
355
 
@@ -369,9 +379,28 @@ export async function verifyDistributionPlatforms({
369
379
  );
370
380
  const subjects = verified.filter((entry) => entry !== undefined);
371
381
 
382
+ // A pinned image absent at its digest is a broken pin, never "not published yet" (no producer lane is about to build
383
+ // it): the failure is rethrown without the registry's `status`, so no caller's not-published-yet skip can take it.
384
+ async function readPublished({ client, repository, subject, pinnedDigest, coordinate }) {
385
+ if (pinnedDigest === undefined) {
386
+ return resolvePublishedPlatforms({ client, repository, tag: subject.contentHash, coordinate });
387
+ }
388
+ let published;
389
+ try {
390
+ published = await resolvePublishedPlatforms({ client, repository, digest: pinnedDigest, coordinate });
391
+ } catch (error) {
392
+ throw new Error(`pinned third-party image ${coordinate} (${subject.kind}:${subject.id}) cannot be read: ${error.message}`, { cause: error });
393
+ }
394
+ if (published.indexDigest !== pinnedDigest) {
395
+ throw new Error(`${coordinate} served manifest ${published.indexDigest} for ${subject.kind}:${subject.id}, not the pinned ${pinnedDigest}`);
396
+ }
397
+ return published;
398
+ }
399
+
372
400
  async function verifySubject(subject) {
373
401
  const { authority, repository } = splitImageRef(subject.imageRef);
374
- const coordinate = `${subject.imageRef}:${subject.contentHash}`;
402
+ const pinnedDigest = pinnedDigests.get(imageSubjectKey(subject));
403
+ const coordinate = pinnedDigest === undefined ? `${subject.imageRef}:${subject.contentHash}` : `${subject.imageRef}@${pinnedDigest}`;
375
404
  // A subject that requires no platform cannot be verified — every check
376
405
  // would pass vacuously and the image would ship unexamined.
377
406
  if (
@@ -384,12 +413,7 @@ export async function verifyDistributionPlatforms({
384
413
  );
385
414
  }
386
415
  const client = clients.get(authority);
387
- const published = await resolvePublishedPlatforms({
388
- client,
389
- repository,
390
- tag: subject.contentHash,
391
- coordinate,
392
- });
416
+ const published = await readPublished({ client, repository, subject, pinnedDigest, coordinate });
393
417
  const present = published.platforms.map((entry) =>
394
418
  platformIdentity(entry.platform),
395
419
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -52,9 +52,9 @@
52
52
  "@xemahq/biome-sdk": "^22.0.0",
53
53
  "@xemahq/biome-supply-chain": "^0.5.0",
54
54
  "@xemahq/distribution-source-hash": "^0.2.2",
55
- "@xemahq/dsl": "^0.24.0",
56
- "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.111.0",
55
+ "@xemahq/dsl": "^0.28.0",
56
+ "@xemahq/identity-client": "^0.36.0",
57
+ "@xemahq/contracts": "^0.112.0",
58
58
  "@xemahq/kernel-state-runtime": "^0.8.0",
59
59
  "@xemahq/license-verifier": "^0.2.0",
60
60
  "@xemahq/service-registry-nest": "^0.12.2",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:5f6bd75aa0d1bdc9a141a6f9f31be0957f7a8e5aa967695362db0ced936cac5a"
95
+ "sourceDigest": "sha256:b0bda6e99522eaf8d1c8c8ad73de127d4c28dd617ca90a3563cbb150a0a980f8"
96
96
  }
97
97
  }
@@ -1,11 +0,0 @@
1
- import { IdentityApiClient } from '@xemahq/identity-client';
2
- export interface RegisterCliClientOptions {
3
- readonly identityApiUrl: string;
4
- readonly internalToken: string;
5
- readonly realm: string;
6
- readonly identityClient?: Pick<IdentityApiClient, 'registerServiceFull'>;
7
- }
8
- export declare const IDENTITY_API_INTERNAL_TOKEN = "IDENTITY_API_INTERNAL_TOKEN";
9
- export declare function resolveIdentityRegistrationToken(configuredToken: string | undefined, environmentToken: string | undefined): string;
10
- export declare function registerCliDeviceClient(options: RegisterCliClientOptions): Promise<void>;
11
- //# sourceMappingURL=client-bootstrap.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/client-bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAIlB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,CAAC;CAC1E;AAaD,eAAO,MAAM,2BAA2B,gCAAgC,CAAC;AAQzE,wBAAgB,gCAAgC,CAC9C,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,MAAM,CAmBR;AAOD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAsBf"}
@@ -1,60 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.IDENTITY_API_INTERNAL_TOKEN = void 0;
4
- exports.resolveIdentityRegistrationToken = resolveIdentityRegistrationToken;
5
- exports.registerCliDeviceClient = registerCliDeviceClient;
6
- const identity_client_1 = require("@xemahq/identity-client");
7
- const device_flow_1 = require("./device-flow");
8
- const PUBLIC_DEVICE_SERVICE_TYPE = 'PUBLIC_DEVICE';
9
- const CLI_REGISTRATION = Object.freeze({
10
- name: device_flow_1.DEFAULT_CLI_CLIENT_ID,
11
- displayName: 'Xema CLI',
12
- description: 'Public device-authorization client for the Xema command-line interface',
13
- serviceType: PUBLIC_DEVICE_SERVICE_TYPE,
14
- regenerateSecret: false,
15
- });
16
- exports.IDENTITY_API_INTERNAL_TOKEN = 'IDENTITY_API_INTERNAL_TOKEN';
17
- function resolveIdentityRegistrationToken(configuredToken, environmentToken) {
18
- const configured = normalizeToken(configuredToken, 'configured secret');
19
- const environment = normalizeToken(environmentToken, 'process environment');
20
- if (configured !== undefined &&
21
- environment !== undefined &&
22
- configured !== environment) {
23
- throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} differs between the configured secret and process environment. Refusing ambiguous identity authority.`);
24
- }
25
- const token = configured ?? environment;
26
- if (token === undefined) {
27
- throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} is required to reconcile the ${device_flow_1.DEFAULT_CLI_CLIENT_ID} public device client. Declare that exact secret or environment variable before launch.`);
28
- }
29
- return token;
30
- }
31
- async function registerCliDeviceClient(options) {
32
- const client = options.identityClient ??
33
- new identity_client_1.IdentityApiClient({
34
- baseUrl: options.identityApiUrl,
35
- internalToken: options.internalToken,
36
- });
37
- const registration = await client.registerServiceFull({
38
- ...CLI_REGISTRATION,
39
- realm: options.realm,
40
- });
41
- if (registration.id !== device_flow_1.DEFAULT_CLI_CLIENT_ID) {
42
- throw new Error(`identity-api registration response did not identify ${device_flow_1.DEFAULT_CLI_CLIENT_ID}.`);
43
- }
44
- if (registration.clientSecret !== null) {
45
- throw new Error(`identity-api returned a client secret for public client ${device_flow_1.DEFAULT_CLI_CLIENT_ID}.`);
46
- }
47
- }
48
- function normalizeToken(token, source) {
49
- if (token === undefined) {
50
- return undefined;
51
- }
52
- if (token.length === 0 || token.trim().length === 0) {
53
- throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} from ${source} is empty.`);
54
- }
55
- if (token !== token.trim()) {
56
- throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} from ${source} has leading or trailing whitespace.`);
57
- }
58
- return token;
59
- }
60
- //# sourceMappingURL=client-bootstrap.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client-bootstrap.js","sourceRoot":"","sources":["../../../src/runtime/auth/client-bootstrap.ts"],"names":[],"mappings":";;;AAmCA,4EAsBC;AAOD,0DAwBC;AAxFD,6DAKiC;AAEjC,+CAAsD;AAStD,MAAM,0BAA0B,GAAgB,eAAe,CAAC;AAEhE,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,mCAAqB;IAC3B,WAAW,EAAE,UAAU;IACvB,WAAW,EACT,wEAAwE;IAC1E,WAAW,EAAE,0BAA0B;IACvC,gBAAgB,EAAE,KAAK;CACwB,CAAC,CAAC;AAEtC,QAAA,2BAA2B,GAAG,6BAA6B,CAAC;AAQzE,SAAgB,gCAAgC,CAC9C,eAAmC,EACnC,gBAAoC;IAEpC,MAAM,UAAU,GAAG,cAAc,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACxE,MAAM,WAAW,GAAG,cAAc,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,CAAC;IAC5E,IACE,UAAU,KAAK,SAAS;QACxB,WAAW,KAAK,SAAS;QACzB,UAAU,KAAK,WAAW,EAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CACb,GAAG,mCAA2B,wGAAwG,CACvI,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,IAAI,WAAW,CAAC;IACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,GAAG,mCAA2B,iCAAiC,mCAAqB,yFAAyF,CAC9K,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAOM,KAAK,UAAU,uBAAuB,CAC3C,OAAiC;IAEjC,MAAM,MAAM,GACV,OAAO,CAAC,cAAc;QACtB,IAAI,mCAAiB,CAAC;YACpB,OAAO,EAAE,OAAO,CAAC,cAAc;YAC/B,aAAa,EAAE,OAAO,CAAC,aAAa;SACrC,CAAC,CAAC;IACL,MAAM,YAAY,GAChB,MAAM,MAAM,CAAC,mBAAmB,CAAC;QAC/B,GAAG,gBAAgB;QACnB,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC,CAAC;IACL,IAAI,YAAY,CAAC,EAAE,KAAK,mCAAqB,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,uDAAuD,mCAAqB,GAAG,CAChF,CAAC;IACJ,CAAC;IACD,IAAI,YAAY,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,2DAA2D,mCAAqB,GAAG,CACpF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CACrB,KAAyB,EACzB,MAAc;IAEd,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,GAAG,mCAA2B,SAAS,MAAM,YAAY,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,KAAK,KAAK,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,GAAG,mCAA2B,SAAS,MAAM,sCAAsC,CACpF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}