@xemahq/xema 0.29.1 → 0.31.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 (62) hide show
  1. package/README.md +60 -389
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/edition.js +3 -3
  6. package/dist/commands/edition.js.map +1 -1
  7. package/dist/dev/infra-preflight.js +1 -1
  8. package/dist/dev/infra-preflight.js.map +1 -1
  9. package/dist/edition/verify-image-evidence.bundle.mjs +10030 -1491
  10. package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
  11. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  12. package/dist/runtime/runner/helm-cli.js.map +1 -1
  13. package/dist/runtime/store-submit.d.ts.map +1 -1
  14. package/dist/runtime/store-submit.js.map +1 -1
  15. package/edition/README.md +50 -271
  16. package/edition/adapter-bundled-versions.mjs +107 -0
  17. package/edition/adapter-host-versions.mjs +41 -0
  18. package/edition/assemble-producer-input.mjs +1 -1
  19. package/edition/authorization-base.mjs +18 -9
  20. package/edition/biome-content.mjs +6 -0
  21. package/edition/build-platform-service-index.mjs +2 -0
  22. package/edition/build-runtime-image-source-index.mjs +97 -9
  23. package/edition/catalogue.mjs +20 -13
  24. package/edition/checkout-indexed-source-repositories.mjs +41 -6
  25. package/edition/customer-definition.mjs +3 -20
  26. package/edition/develop-customer-edition.mjs +2 -2
  27. package/edition/develop-edition-resolve.mjs +10 -2
  28. package/edition/develop-edition.mjs +56 -62
  29. package/edition/develop-freeze.mjs +14 -3
  30. package/edition/dispatched-runtime-components.mjs +82 -0
  31. package/edition/dist-check.mjs +1 -0
  32. package/edition/distribution-evidence-set.mjs +32 -5
  33. package/edition/edition-channel.mjs +15 -1
  34. package/edition/edition-locks.mjs +16 -6
  35. package/edition/edition-protocol.mjs +30 -8
  36. package/edition/edition-workspace.mjs +8 -0
  37. package/edition/external-source-walk.mjs +1 -1
  38. package/edition/image-evidence.mjs +134 -26
  39. package/edition/kernel-capability-domain.mjs +30 -0
  40. package/edition/kernel-package.mjs +193 -0
  41. package/edition/lib.mjs +192 -4
  42. package/edition/oci-evidence-verbs.mjs +2 -2
  43. package/edition/operation-grant-admission.mjs +70 -0
  44. package/edition/operation-requirements.mjs +128 -0
  45. package/edition/plan-image-evidence-refresh.mjs +2 -2
  46. package/edition/platform-lock-readability.mjs +2 -2
  47. package/edition/platform-service-class.mjs +42 -0
  48. package/edition/platform-service-ownership.mjs +78 -0
  49. package/edition/produce-distribution-completeness-evidence.mjs +5 -0
  50. package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
  51. package/edition/release-catalogue.mjs +10 -3
  52. package/edition/remedy-command.mjs +2 -2
  53. package/edition/resolve-chart-set.mjs +1 -1
  54. package/edition/resolve-customer-edition.mjs +33 -34
  55. package/edition/resolve-distribution.mjs +70 -16
  56. package/edition/resolve-edition.mjs +3 -2
  57. package/edition/resolve-images.mjs +65 -4
  58. package/edition/seal-image-digests.mjs +17 -5
  59. package/edition/trust-policy-arming-verb.mjs +2 -10
  60. package/edition/verify-image-evidence.mjs +1 -0
  61. package/examples/README.md +3 -2
  62. package/package.json +4 -4
@@ -2,7 +2,7 @@
2
2
  // it too.
3
3
  //
4
4
  // The kernel (`@xemahq/contracts/edition-protocol`) exports the identity of the
5
- // code this library executes as DATA: `{contentDigest, version}` in
5
+ // code this library executes as DATA: `{contentDigest, provisional, version}` in
6
6
  // `dist/edition-protocol/edition-protocol.json`, and the edition contract in
7
7
  // `dist/edition-protocol/edition-contract.schema.json` (the identity, the
8
8
  // hashing convention, the entry subpaths this library may load, and every
@@ -31,8 +31,8 @@ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
31
31
  /** The edition protocols this library interprets: version -> content digest. */
32
32
  export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
33
33
  Object.freeze({
34
- version: 1,
35
- contentDigest: 'd5b7ce85aa6596730527da64a7f6682684039398a88fd168429f6e4a3f0438bd',
34
+ version: 5,
35
+ contentDigest: '326d8b62ba87073fa91cf726eb596f9371904f056db688729d8800cc94c2d646',
36
36
  }),
37
37
  ]);
38
38
 
@@ -52,8 +52,10 @@ const SHA256_HEX = /^[a-f0-9]{64}$/;
52
52
  export const EditionProtocolMismatchReason = Object.freeze({
53
53
  /** The kernel publishes no edition protocol identity. */
54
54
  Undeclared: 'undeclared',
55
- /** The identity file is not exactly `{contentDigest, version}`. */
55
+ /** The identity file is not exactly `{contentDigest, provisional, version}`. */
56
56
  Malformed: 'malformed',
57
+ /** The identity states `provisional: true`: a branch-local working identity, never pinned. */
58
+ Provisional: 'provisional',
57
59
  /** This library supports no protocol of that version. */
58
60
  UnsupportedVersion: 'unsupported-version',
59
61
  /** The version is supported, but pinned to a different digest. */
@@ -77,7 +79,20 @@ const describe = ({ version, contentDigest }) => `edition protocol v${version} (
77
79
  const supportedList = () =>
78
80
  SUPPORTED_EDITION_PROTOCOLS.map((protocol) => `v${protocol.version}`).join(', ');
79
81
 
80
- /** Parse identity bytes strictly: exactly a positive integer version and a lowercase SHA-256 hex digest. */
82
+ /** Refuse an identity that states `provisional: true`; nothing provisional is ever read as a protocol. */
83
+ function assertFinalIdentity(identity, label) {
84
+ if (identity.provisional !== false) {
85
+ throw new EditionProtocolMismatchError(
86
+ EditionProtocolMismatchReason.Provisional,
87
+ `${label} declares ${describe(identity)} as provisional (a branch-local working identity); a provisional edition protocol is never pinned`,
88
+ );
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Parse identity bytes strictly: exactly `{contentDigest, provisional, version}`, a positive integer version, a
94
+ * lowercase SHA-256 hex digest and a boolean `provisional`. An identity stating `provisional: true` is refused.
95
+ */
81
96
  export function parseEditionProtocolIdentity(bytes, label) {
82
97
  let identity;
83
98
  try {
@@ -92,7 +107,8 @@ export function parseEditionProtocolIdentity(bytes, label) {
92
107
  identity === null ||
93
108
  typeof identity !== 'object' ||
94
109
  Array.isArray(identity) ||
95
- Object.keys(identity).sort().join() !== 'contentDigest,version' ||
110
+ Object.keys(identity).sort().join() !== 'contentDigest,provisional,version' ||
111
+ typeof identity.provisional !== 'boolean' ||
96
112
  !Number.isSafeInteger(identity.version) ||
97
113
  identity.version < 1 ||
98
114
  typeof identity.contentDigest !== 'string' ||
@@ -100,10 +116,15 @@ export function parseEditionProtocolIdentity(bytes, label) {
100
116
  ) {
101
117
  throw new EditionProtocolMismatchError(
102
118
  EditionProtocolMismatchReason.Malformed,
103
- `${label} must be exactly {"contentDigest":"<64 lowercase hex>","version":<positive integer>}`,
119
+ `${label} must be exactly {"contentDigest":"<64 lowercase hex>","provisional":<boolean>,"version":<positive integer>}`,
104
120
  );
105
121
  }
106
- return Object.freeze({ contentDigest: identity.contentDigest, version: identity.version });
122
+ assertFinalIdentity(identity, label);
123
+ return Object.freeze({
124
+ contentDigest: identity.contentDigest,
125
+ provisional: identity.provisional,
126
+ version: identity.version,
127
+ });
107
128
  }
108
129
 
109
130
  /** The `{version, contentDigest}` a contract file states, or undefined. */
@@ -216,6 +237,7 @@ export function assertKernelEditionProtocol(kernelPackageDir) {
216
237
  * it, so the kernel the workspace states must speak exactly that protocol.
217
238
  */
218
239
  export function assertKernelSpeaksBundledProtocol({ kernelPackageDir, bundled }) {
240
+ assertFinalIdentity(bundled, 'the edition protocol compiled into this verifier');
219
241
  const stated = readKernelEditionProtocol(kernelPackageDir);
220
242
  if (stated.version !== bundled.version || stated.contentDigest !== bundled.contentDigest) {
221
243
  throw new EditionProtocolMismatchError(
@@ -70,6 +70,14 @@ export function configureEditionWorkspace(roots) {
70
70
  return configured;
71
71
  }
72
72
 
73
+ /**
74
+ * State the Kernel root AFTER configuration, for a customer verb that takes its kernel from the catalogue
75
+ * (kernel-package.mjs) rather than from a flag. Every other configured root is kept; there is still no default.
76
+ */
77
+ export function stateKernelPackageDir(dir) {
78
+ configured = Object.freeze({ ...configured, [WorkspaceRoot.Kernel]: assertDirectory(WorkspaceRoot.Kernel, dir) });
79
+ }
80
+
73
81
  function root(name) {
74
82
  const value = configured[name];
75
83
  if (value === undefined) {
@@ -376,7 +376,7 @@ function lockedSourceRecord(source, commit) {
376
376
  // DistributionLockSchema is `.strict()` and only learned this key in 0.10.0,
377
377
  // so any pinned image resolving `^0.9.x` REFUSES THE WHOLE LOCK at boot —
378
378
  // `Unrecognized key: "imageNamePrefix"`, before Nest starts. That took
379
- // presenca-dev's three lock consumers down on 2026-09-17 and was fixed by
379
+ // a customer environment's three lock consumers down on 2026-09-17 and was fixed by
380
380
  // removing the key from the SEALED LOCK, leaving this line to re-emit it on
381
381
  // the next resolve. A workaround standing where a fix belongs removes the
382
382
  // signal; this is the fix.
@@ -593,6 +593,13 @@ export function validateEntitlementStatementBinding({
593
593
  return statement;
594
594
  }
595
595
 
596
+ /** Throw the kernel's refusal code, verbatim, when a kernel predicate check returned one. */
597
+ function assertKernelPredicateAccepted(label, refusal) {
598
+ if (refusal !== undefined) {
599
+ throw new Error(`${label} is refused: ${refusal}`);
600
+ }
601
+ }
602
+
596
603
  function assertEvidenceChannel(channel) {
597
604
  if (typeof channel !== 'string' || channel === '') {
598
605
  throw new Error('evidence verification requires the channel of the edition being verified');
@@ -670,6 +677,7 @@ export function verifySigstoreEvidenceDescriptor({
670
677
  parseStatement,
671
678
  policyRoot = evidenceRoot,
672
679
  trustedCosign,
680
+ holdProducerWorkflow,
673
681
  }) {
674
682
  assertEvidenceChannel(channel);
675
683
  const statementFile = readExactDescriptorFile(
@@ -744,6 +752,7 @@ export function verifySigstoreEvidenceDescriptor({
744
752
  statement.predicate?.producerRun?.producerWorkflowSha;
745
753
  let verified = false;
746
754
  let verifiedIdentityPolicy;
755
+ let verifiedCandidateWorkflowSha;
747
756
  const verificationErrors = [];
748
757
  for (const candidate of candidates) {
749
758
  if (!verifiedWorkflowSha) {
@@ -806,6 +815,7 @@ export function verifySigstoreEvidenceDescriptor({
806
815
  });
807
816
  verified = true;
808
817
  verifiedIdentityPolicy = candidate;
818
+ verifiedCandidateWorkflowSha = verifiedWorkflowSha;
809
819
  break;
810
820
  } catch (error) {
811
821
  verificationErrors.push(
@@ -826,6 +836,16 @@ export function verifySigstoreEvidenceDescriptor({
826
836
  verificationErrors.join('; '),
827
837
  );
828
838
  }
839
+ // The certificate's workflow commit is held to the data root's operator-stated pin for that workflow's repository.
840
+ if (holdProducerWorkflow !== undefined) {
841
+ assertKernelPredicateAccepted(
842
+ `${predicateType} signer ${verifiedIdentityPolicy.githubWorkflow.repository}`,
843
+ holdProducerWorkflow({
844
+ repository: verifiedIdentityPolicy.githubWorkflow.repository,
845
+ workflowCommit: verifiedCandidateWorkflowSha,
846
+ }),
847
+ );
848
+ }
829
849
  return {
830
850
  statement,
831
851
  statementPath: statementFile.path,
@@ -874,10 +894,12 @@ async function verifyDistributionImageEvidenceSetWithCosign({
874
894
  distributionLock,
875
895
  imageLock,
876
896
  repositoryPins,
877
- runtimeSourceIndex: { contractAuthority: expectedContractAuthority },
897
+ runtimeSourceIndex,
878
898
  } = await loadValidatedLocks(distributionId, dataRoot, schemas, {
879
899
  editionsRoot,
900
+ channel,
880
901
  });
902
+ const expectedContractAuthority = runtimeSourceIndex.contractAuthority;
881
903
  if (evidenceSet.distributionId !== distributionId) {
882
904
  throw new Error(
883
905
  `evidence set "${evidenceSet.distributionId}" does not match requested distribution "${distributionId}"`,
@@ -922,6 +944,7 @@ async function verifyDistributionImageEvidenceSetWithCosign({
922
944
  parseStatement: schemas.parseImageProducerStatement,
923
945
  policyRoot,
924
946
  trustedCosign,
947
+ holdProducerWorkflow: (producer) => schemas.producerWorkflowPinRefusal(runtimeSourceIndex, producer),
925
948
  });
926
949
  validateProducerStatementBinding({
927
950
  statement,
@@ -943,7 +966,7 @@ async function verifyDistributionImageEvidenceSetWithCosign({
943
966
  }
944
967
  }
945
968
 
946
- const { statement: completenessStatement } = verifySigstoreEvidenceDescriptor(
969
+ const { statement: completenessStatement, identityPolicy: completenessPolicy } = verifySigstoreEvidenceDescriptor(
947
970
  {
948
971
  evidenceRoot,
949
972
  descriptor: evidenceSet.completeness,
@@ -956,6 +979,16 @@ async function verifyDistributionImageEvidenceSetWithCosign({
956
979
  trustedCosign,
957
980
  },
958
981
  );
982
+ // After the signature verified: the channel the predicate states is the verifying policy row's, and the
983
+ // predicate completes THIS lock. A kernel refusal is terminal and is surfaced by its code.
984
+ assertKernelPredicateAccepted(
985
+ `distribution completeness for ${imageLock.distributionId}`,
986
+ schemas.distributionImagesPredicateChannelRefusal(completenessStatement.predicate, completenessPolicy),
987
+ );
988
+ assertKernelPredicateAccepted(
989
+ `distribution completeness for ${imageLock.distributionId}`,
990
+ schemas.distributionImagesPredicateLockRefusal(completenessStatement.predicate, distributionLock),
991
+ );
959
992
  validateCompletenessStatementBinding({
960
993
  statement: completenessStatement,
961
994
  distributionLock,
@@ -1002,6 +1035,7 @@ async function verifyImageDeliveryEntitlementWithCosign({
1002
1035
  completenessStatementDigest,
1003
1036
  customerId,
1004
1037
  distributionId,
1038
+ distributionLock,
1005
1039
  channel,
1006
1040
  mode,
1007
1041
  targetRegistry,
@@ -1023,6 +1057,10 @@ async function verifyImageDeliveryEntitlementWithCosign({
1023
1057
  policyRoot,
1024
1058
  trustedCosign,
1025
1059
  });
1060
+ assertKernelPredicateAccepted(
1061
+ `delivery entitlement for ${customerId}`,
1062
+ schemas.imageDeliveryEntitlementRefusal(statement.predicate, distributionLock),
1063
+ );
1026
1064
  validateEntitlementStatementBinding({
1027
1065
  statement,
1028
1066
  completenessStatementDigest,
@@ -1044,7 +1082,7 @@ async function verifyImageDeliveryEntitlementWithCosign({
1044
1082
  * `submodules/…` repository the vendor builds images from. A customer edition
1045
1083
  * additionally draws biomes from sources declared in its own manifest
1046
1084
  * (`sources[]`, `kind: git`), and the images built from those have a `repo`
1047
- * the index has never heard of and never will: `presenca-biomes`, not
1085
+ * the index has never heard of and never will: `<customer>-biomes`, not
1048
1086
  * `repos/xema-base`. Without a pin for it every such subject is refused as
1049
1087
  * "has no committed repository pin", so a customer could verify the vendor's
1050
1088
  * images in its edition and never its own.
@@ -1124,20 +1162,57 @@ export function externalSourceRepositoryPins({
1124
1162
  * tolerant read-only posture `parseDistributionLockForReading` gives a
1125
1163
  * runtime, without depending on which kernel the bundle was built from.
1126
1164
  *
1127
- * `schemaVersion` must be 5, the one grammar there is (the kernel's
1128
- * `DISTRIBUTION_LOCK_SCHEMA_VERSIONS`): a pre-v5 lock is refused, with no
1129
- * compatibility path.
1165
+ * Two grammars exist, and which one is accepted is decided by `channel`, the
1166
+ * kernel evidence channel (`EVIDENCE_CHANNELS`) the caller operates on; it is
1167
+ * required and has no default. An UNSEALED lock (it carries `unsealedSchema`;
1168
+ * every edition lock on develop is one by design) is accepted ONLY on the
1169
+ * develop channel, read through the kernel's `parseUnsealedDistributionLock`.
1170
+ * Every other channel requires a SEALED lock: images sealed over an unsealed
1171
+ * lock is the crash state every sealed reader refuses, and a release statement
1172
+ * must never verify over it. A SEALED lock must have `schemaVersion` 5 (the
1173
+ * kernel's `DISTRIBUTION_LOCK_SCHEMA_VERSIONS`): a pre-v5 lock is refused, with
1174
+ * no compatibility path. Both carry `distributionId` and `inputHash`, and
1175
+ * sealing preserves them, so either way the binding is the same two fields.
1130
1176
  */
1131
- export function readDistributionLockBinding(raw) {
1177
+ /** The one evidence channel an unsealed distribution lock is read on. */
1178
+ const UNSEALED_LOCK_CHANNEL = 'develop';
1179
+
1180
+ /**
1181
+ * The channel a distribution lock is read for: one of the kernel's
1182
+ * `EVIDENCE_CHANNELS`, stated by the caller. Missing or unknown is refused.
1183
+ */
1184
+ function assertLockReadingChannel(channel, schemas) {
1185
+ if (!schemas) {
1186
+ throw new Error('trusted kernel distribution schemas are required');
1187
+ }
1188
+ if (!schemas.EVIDENCE_CHANNELS.includes(channel)) {
1189
+ throw new Error(
1190
+ `a distribution lock is read for one evidence channel (${schemas.EVIDENCE_CHANNELS.join(' | ')}); got ${JSON.stringify(channel)}`,
1191
+ );
1192
+ }
1193
+ }
1194
+
1195
+ export function readDistributionLockBinding(raw, schemas, channel) {
1196
+ assertLockReadingChannel(channel, schemas);
1132
1197
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
1133
1198
  throw new Error('distribution.lock.json is not a JSON object');
1134
1199
  }
1135
- const { schemaVersion, distributionId, inputHash } = raw;
1136
- if (schemaVersion !== 5) {
1200
+ let source = raw;
1201
+ if ('unsealedSchema' in raw) {
1202
+ if (channel !== UNSEALED_LOCK_CHANNEL) {
1203
+ throw new Error(
1204
+ `distribution.lock.json is unsealed; the ${channel} channel accepts only a sealed lock (schemaVersion 5) — ` +
1205
+ `an unsealed lock is read on the ${UNSEALED_LOCK_CHANNEL} channel only`,
1206
+ );
1207
+ }
1208
+ source = schemas.parseUnsealedDistributionLock(raw);
1209
+ } else if (raw.schemaVersion !== 5) {
1137
1210
  throw new Error(
1138
- `distribution.lock.json has schemaVersion ${String(schemaVersion)}; the only accepted grammar is 5 (regenerate the lock)`,
1211
+ `distribution.lock.json is neither an unsealed lock (carrying unsealedSchema) ` +
1212
+ `nor a sealed lock (schemaVersion 5); it has schemaVersion ${String(raw.schemaVersion)} (regenerate the lock)`,
1139
1213
  );
1140
1214
  }
1215
+ const { distributionId, inputHash } = source;
1141
1216
  // The id is format-checked only. Which edition it must equal is decided by
1142
1217
  // the bindings that USE it — the completeness predicate and the evidence set
1143
1218
  // compare it to the image lock and the signed statement — exactly as before
@@ -1149,21 +1224,63 @@ export function readDistributionLockBinding(raw) {
1149
1224
  'expected 64 lowercase sha256 hex characters',
1150
1225
  );
1151
1226
  }
1152
- return Object.freeze({ schemaVersion, distributionId, inputHash });
1227
+ return Object.freeze({ distributionId, inputHash });
1228
+ }
1229
+
1230
+ /**
1231
+ * The edition's sealed `distribution.lock.json`, parsed by the kernel's strict lock parser: the document the
1232
+ * predicates' `lockDigest` is computed over. Held to the binding rules of {@link readDistributionLockBinding} first.
1233
+ */
1234
+ export function readEditionDistributionLock(editionsRoot, distributionId, schemas, channel) {
1235
+ assertLockReadingChannel(channel, schemas);
1236
+ const raw = JSON.parse(
1237
+ readFileSync(join(editionsRoot, distributionId, 'distribution.lock.json'), 'utf8'),
1238
+ );
1239
+ readDistributionLockBinding(raw, schemas, channel);
1240
+ // The WHOLE lock in its own grammar, never the two-field binding: the
1241
+ // predicate and entitlement refusals recompute distributionLockDigest over
1242
+ // what this returns, so an unsealed develop lock is digested as itself.
1243
+ return 'unsealedSchema' in raw ? schemas.parseUnsealedDistributionLock(raw) : schemas.parseDistributionLock(raw);
1153
1244
  }
1154
1245
 
1155
1246
  /**
1156
1247
  * Read and validate an edition's two locks plus the runtime image source index
1157
- * they must agree with.
1248
+ * they must agree with. The distribution lock is read for `channel`
1249
+ * ({@link readDistributionLockBinding}); the rest is {@link loadValidatedImageLocks}.
1250
+ */
1251
+ export async function loadValidatedLocks(
1252
+ distributionId,
1253
+ dataRoot,
1254
+ schemas,
1255
+ { editionsRoot, channel },
1256
+ ) {
1257
+ if (!schemas) {
1258
+ throw new Error('trusted kernel distribution schemas are required');
1259
+ }
1260
+ const distributionLock = readEditionDistributionLock(
1261
+ editionsRoot,
1262
+ distributionId,
1263
+ schemas,
1264
+ channel,
1265
+ );
1266
+ return {
1267
+ distributionLock,
1268
+ ...(await loadValidatedImageLocks(distributionId, dataRoot, schemas, { editionsRoot })),
1269
+ };
1270
+ }
1271
+
1272
+ /**
1273
+ * Read and validate an edition's image lock plus the runtime image source index
1274
+ * it must agree with; the distribution lock is not read.
1158
1275
  *
1159
1276
  * `dataRoot` holds the runtime image source index, read from it always.
1160
- * `editionsRoot` is where `<id>/distribution.lock.json`,
1161
- * `<id>/images.lock.json` and `<id>/xema-distribution.json` are read from. The
1162
- * manifest extends the repository pins with the edition's own declared sources
1277
+ * `editionsRoot` is where `<id>/images.lock.json` and
1278
+ * `<id>/xema-distribution.json` are read from. The manifest extends the
1279
+ * repository pins with the edition's own declared sources
1163
1280
  * ({@link externalSourceRepositoryPins}); the verifier grades the editions
1164
- * checkout so all three files are HEAD-bound.
1281
+ * checkout so the files it reads are HEAD-bound.
1165
1282
  */
1166
- export async function loadValidatedLocks(
1283
+ export async function loadValidatedImageLocks(
1167
1284
  distributionId,
1168
1285
  dataRoot,
1169
1286
  schemas,
@@ -1172,14 +1289,6 @@ export async function loadValidatedLocks(
1172
1289
  if (!schemas) {
1173
1290
  throw new Error('trusted kernel distribution schemas are required');
1174
1291
  }
1175
- const distributionLock = readDistributionLockBinding(
1176
- JSON.parse(
1177
- readFileSync(
1178
- join(editionsRoot, distributionId, 'distribution.lock.json'),
1179
- 'utf8',
1180
- ),
1181
- ),
1182
- );
1183
1292
  const imageLock = validateImmutableImageLock(
1184
1293
  schemas.parseImageLock(
1185
1294
  JSON.parse(
@@ -1235,7 +1344,6 @@ export async function loadValidatedLocks(
1235
1344
  }
1236
1345
  return {
1237
1346
  schemas,
1238
- distributionLock,
1239
1347
  imageLock,
1240
1348
  runtimeSourceIndex,
1241
1349
  repositoryPins,
@@ -0,0 +1,30 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ import { kernelDistPath } from './edition-workspace.mjs';
4
+
5
+ const requireFromHere = createRequire(import.meta.url);
6
+
7
+ let schema;
8
+
9
+ /**
10
+ * Is `value` a capability domain? The kernel's `CapabilityDomainSchema` is the
11
+ * ONE grammar; it is read from the distribution subpath, the only plane the
12
+ * edition protocol exposes to this library. Nothing here restates the pattern.
13
+ *
14
+ * @param {unknown} value
15
+ * @returns {boolean}
16
+ */
17
+ export function isKernelCapabilityDomain(value) {
18
+ if (schema === undefined) {
19
+ const path = kernelDistPath('distribution', 'index.js');
20
+ const mod = requireFromHere(path);
21
+ if (typeof mod.CapabilityDomainSchema?.safeParse !== 'function') {
22
+ throw new Error(
23
+ `Built kernel distribution module at ${path} does not export CapabilityDomainSchema — ` +
24
+ 'the dist is stale; rebuild kernel-contracts at a commit that carries it.',
25
+ );
26
+ }
27
+ schema = mod.CapabilityDomainSchema;
28
+ }
29
+ return schema.safeParse(value).success;
30
+ }
@@ -0,0 +1,193 @@
1
+ // kernel-package — the customer edition's kernel: `@xemahq/contracts` taken from public npm.
2
+ //
3
+ // A catalogue names its kernel package (`kernel: {name, version, integrity}`, catalogue.mjs). This module is
4
+ // the ONE place that touches npm: it downloads that exact tarball (public, no credential, no .npmrc, no
5
+ // npm/pnpm process), holds it by the catalogue's integrity BEFORE anything is extracted, extracts it with the
6
+ // system `tar`, links its runtime dependencies to the copies this CLI itself ships, and proves the package is
7
+ // the contract authority the catalogue was resolved under: first by its edition protocol identity (data, no
8
+ // code executed), and only then, by loading its schemas, by the constants the authority was derived with.
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import { createHash } from 'node:crypto';
12
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { tmpdir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+ import { isDeepStrictEqual } from 'node:util';
17
+
18
+ import { readKernelEditionProtocol } from './edition-protocol.mjs';
19
+ import { cacheDir, stateKernelPackageDir } from './edition-workspace.mjs';
20
+ import { loadKernelDistributionSchemas } from './lib.mjs';
21
+
22
+ export const KERNEL_PACKAGE_NAME = '@xemahq/contracts';
23
+ const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
24
+ const DEPENDENCY_NAME = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
25
+ const EXACT = /^(\d+)\.(\d+)\.(\d+)$/;
26
+ const CARET = /^\^(\d+)\.(\d+)\.(\d+)$/;
27
+
28
+ export class KernelPackageError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = 'KernelPackageError';
32
+ }
33
+ }
34
+
35
+ export const kernelTarballUrl = (version) => `https://registry.npmjs.org/${KERNEL_PACKAGE_NAME}/-/contracts-${version}.tgz`;
36
+
37
+ /** The npm integrity string of `bytes`: `sha512-<base64>`. */
38
+ export const integrityOf = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
39
+
40
+ /** The published tarball of `@xemahq/contracts@version`, bounded in size. */
41
+ export async function fetchKernelTarball(version, { fetch = globalThis.fetch } = {}) {
42
+ const url = kernelTarballUrl(version);
43
+ const response = await fetch(url);
44
+ if (response.status === 404) {
45
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME}@${version} is not published on npm (${url}): a catalogue names a published kernel`);
46
+ }
47
+ if (!response.ok) {throw new KernelPackageError(`${url} answered HTTP ${response.status}`);}
48
+ const declared = Number(response.headers.get('content-length') ?? 0);
49
+ if (declared > MAX_TARBALL_BYTES) {throw new KernelPackageError(`${url} declares ${declared} bytes, over the ${MAX_TARBALL_BYTES} byte bound`);}
50
+ const chunks = [];
51
+ let size = 0;
52
+ for await (const chunk of response.body) {
53
+ size += chunk.length;
54
+ if (size > MAX_TARBALL_BYTES) {throw new KernelPackageError(`${url} is over the ${MAX_TARBALL_BYTES} byte bound`);}
55
+ chunks.push(chunk);
56
+ }
57
+ return Buffer.concat(chunks);
58
+ }
59
+
60
+ /**
61
+ * Does `version` (exactly x.y.z) satisfy `range`? Exactly two range forms are supported: `x.y.z`, and
62
+ * `^x.y.z` (x > 0: >=x.y.z <(x+1).0.0; 0.y.z: >=0.y.z <0.(y+1).0). Any other form is refused.
63
+ */
64
+ export function satisfiesDeclaredRange(version, range) {
65
+ const have = EXACT.exec(version)?.slice(1).map(Number);
66
+ if (have === undefined) {throw new KernelPackageError(`version ${JSON.stringify(version)} is not an exact x.y.z version`);}
67
+ const exact = EXACT.exec(range)?.slice(1).map(Number);
68
+ if (exact !== undefined) {return isDeepStrictEqual(have, exact);}
69
+ const caret = CARET.exec(range)?.slice(1).map(Number);
70
+ if (caret === undefined) {throw new KernelPackageError(`range ${JSON.stringify(range)} is not x.y.z or ^x.y.z, the only forms a kernel dependency may declare`);}
71
+ const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
72
+ const upper = caret[0] > 0 ? [caret[0] + 1, 0, 0] : [0, caret[1] + 1, 0];
73
+ return compare(have, caret) >= 0 && compare(have, upper) < 0;
74
+ }
75
+
76
+ /** The directory of `name` as this CLI itself ships it: its own resolution first, then its installed kernel's. */
77
+ function shippedPackageDir(name) {
78
+ const own = createRequire(import.meta.url);
79
+ const attempts = [() => own, () => createRequire(realpathSync(own.resolve(`${KERNEL_PACKAGE_NAME}/package.json`)))];
80
+ for (const requireFrom of attempts) {
81
+ try {
82
+ return dirname(realpathSync(requireFrom().resolve(`${name}/package.json`)));
83
+ } catch (error) {
84
+ if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {throw error;}
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function linkRuntimeDependencies(packageDir) {
91
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
92
+ const nodeModules = dirname(manifest.name.startsWith('@') ? dirname(packageDir) : packageDir);
93
+ for (const [name, range] of Object.entries(manifest.dependencies ?? {})) {
94
+ if (!DEPENDENCY_NAME.test(name)) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} declares a dependency named ${JSON.stringify(name)}, which is not a package name`);}
95
+ const shipped = shippedPackageDir(name);
96
+ if (shipped === undefined) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} depends on ${name}@${range}, which this CLI does not ship`);}
97
+ const version = JSON.parse(readFileSync(join(shipped, 'package.json'), 'utf8')).version;
98
+ let ok;
99
+ try {
100
+ ok = satisfiesDeclaredRange(version, range);
101
+ } catch (error) {
102
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} dependency ${name}: ${error.message} (this CLI ships ${version})`);
103
+ }
104
+ if (!ok) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} depends on ${name}@${range}, but this CLI ships ${name}@${version}`);}
105
+ const link = join(nodeModules, name);
106
+ mkdirSync(dirname(link), { recursive: true });
107
+ symlinkSync(shipped, link, 'dir');
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Download the catalogue's kernel, hold it to `kernel.integrity` (refused before anything is written), extract it
113
+ * under `<cacheDir>/kernel-npm/<version>-<16 hex of the sha512>/node_modules/@xemahq/contracts` (fresh every run)
114
+ * and link its runtime dependencies. Returns the package directory.
115
+ */
116
+ export async function materializeKernelPackage({ kernel, cacheDir, fetchTarball = fetchKernelTarball }) {
117
+ const bytes = await fetchTarball(kernel.version);
118
+ const actual = integrityOf(bytes);
119
+ if (actual !== kernel.integrity) {
120
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME}@${kernel.version} integrity ${actual} is not the catalogue's kernel.integrity ${kernel.integrity}`);
121
+ }
122
+ const hex = createHash('sha512').update(bytes).digest('hex').slice(0, 16);
123
+ const root = join(cacheDir, 'kernel-npm', `${kernel.version}-${hex}`);
124
+ const packageDir = join(root, 'node_modules', ...KERNEL_PACKAGE_NAME.split('/'));
125
+ rmSync(root, { recursive: true, force: true });
126
+ mkdirSync(packageDir, { recursive: true });
127
+ const extracted = spawnSync('tar', ['-xzf', '-', '-C', packageDir, '--strip-components', '1'], { input: bytes, stdio: ['pipe', 'ignore', 'pipe'] });
128
+ if (extracted.error !== undefined) {throw new KernelPackageError(`tar could not run: ${extracted.error.message}`);}
129
+ if (extracted.status !== 0) {throw new KernelPackageError(`tar failed extracting ${KERNEL_PACKAGE_NAME}@${kernel.version}: ${extracted.stderr.toString().trim()}`);}
130
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
131
+ if (manifest.name !== KERNEL_PACKAGE_NAME || manifest.version !== kernel.version) {
132
+ throw new KernelPackageError(`the tarball holds ${manifest.name}@${manifest.version}, not ${KERNEL_PACKAGE_NAME}@${kernel.version}`);
133
+ }
134
+ linkRuntimeDependencies(packageDir);
135
+ return packageDir;
136
+ }
137
+
138
+ const refuse = (field, wanted, got) => new KernelPackageError(`the kernel package is not the catalogue's contract authority: ${field} is ${JSON.stringify(got)}, the catalogue states ${JSON.stringify(wanted)}`);
139
+
140
+ /**
141
+ * The kernel package at `kernelPackageDir` is the contract authority of `catalogue`. First its identity, read as
142
+ * data (final, which the protocol read refuses otherwise, and the catalogue's digest); only then are its schemas loaded and held to the authority's constants.
143
+ * `sourceCommit` is provenance and is not compared. Returns the loaded schemas.
144
+ */
145
+ export async function assertKernelPackageIsCatalogueAuthority({ catalogue, kernelPackageDir, loadSchemas }) {
146
+ const authority = catalogue.contractAuthority;
147
+ const identity = readKernelEditionProtocol(kernelPackageDir);
148
+ if (identity.contentDigest !== authority.contentDigest) {throw refuse('contentDigest', authority.contentDigest, identity.contentDigest);}
149
+ const schemas = await loadSchemas();
150
+ if (!isDeepStrictEqual(authority.sourcePaths, schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS)) {
151
+ throw refuse('sourcePaths', authority.sourcePaths, schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS);
152
+ }
153
+ const constants = [
154
+ ['generatorIdentity', schemas.XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR],
155
+ ['repositoryUrl', schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL],
156
+ ['contentHashAlgorithm', schemas.KernelContractContentHashAlgorithm.LengthFramedGitPathModeBlobV1],
157
+ ];
158
+ for (const [field, kernelValue] of constants) {
159
+ if (authority[field] !== kernelValue) {throw refuse(field, authority[field], kernelValue);}
160
+ }
161
+ return schemas;
162
+ }
163
+
164
+ /**
165
+ * The `kernel` row a VENDOR catalogue carries: the published `@xemahq/contracts` the vendor's own kernel
166
+ * (`kernelPackageDir`) is, taken from npm, proved to be `contractAuthority` and held by its integrity.
167
+ * An unpublished version is refused. The downloaded package is extracted to a temporary directory and removed.
168
+ */
169
+ export async function kernelPackageOfAuthority({ kernelPackageDir, contractAuthority, fetchTarball = fetchKernelTarball, loadSchemas = loadKernelDistributionSchemas }) {
170
+ const manifest = JSON.parse(readFileSync(join(kernelPackageDir, 'package.json'), 'utf8'));
171
+ if (manifest.name !== KERNEL_PACKAGE_NAME) {throw new KernelPackageError(`the vendor kernel is ${manifest.name}, not ${KERNEL_PACKAGE_NAME}`);}
172
+ const bytes = await fetchTarball(manifest.version);
173
+ const kernel = { name: KERNEL_PACKAGE_NAME, version: manifest.version, integrity: integrityOf(bytes) };
174
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-kernel-npm-'));
175
+ try {
176
+ const packageDir = await materializeKernelPackage({ kernel, cacheDir: scratch, fetchTarball: async () => bytes });
177
+ await assertKernelPackageIsCatalogueAuthority({ catalogue: { contractAuthority }, kernelPackageDir: packageDir, loadSchemas });
178
+ } finally {
179
+ rmSync(scratch, { recursive: true, force: true });
180
+ }
181
+ return kernel;
182
+ }
183
+
184
+ /**
185
+ * A customer verb's one step between fetching its catalogue and using any kernel schema: the catalogue's kernel is
186
+ * downloaded into the cache root, stated as the workspace's Kernel root, and proved to be the catalogue's contract
187
+ * authority. Returns the kernel's distribution schemas.
188
+ */
189
+ export async function adoptCatalogueKernel(catalogue) {
190
+ const kernelPackageDir = await materializeKernelPackage({ kernel: catalogue.kernel, cacheDir: cacheDir() });
191
+ stateKernelPackageDir(kernelPackageDir);
192
+ return assertKernelPackageIsCatalogueAuthority({ catalogue, kernelPackageDir, loadSchemas: loadKernelDistributionSchemas });
193
+ }