@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.
- package/README.md +60 -389
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/infra-preflight.js +1 -1
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +10030 -1491
- package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/store-submit.d.ts.map +1 -1
- package/dist/runtime/store-submit.js.map +1 -1
- package/edition/README.md +50 -271
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/assemble-producer-input.mjs +1 -1
- package/edition/authorization-base.mjs +18 -9
- package/edition/biome-content.mjs +6 -0
- package/edition/build-platform-service-index.mjs +2 -0
- package/edition/build-runtime-image-source-index.mjs +97 -9
- package/edition/catalogue.mjs +20 -13
- package/edition/checkout-indexed-source-repositories.mjs +41 -6
- package/edition/customer-definition.mjs +3 -20
- package/edition/develop-customer-edition.mjs +2 -2
- package/edition/develop-edition-resolve.mjs +10 -2
- package/edition/develop-edition.mjs +56 -62
- package/edition/develop-freeze.mjs +14 -3
- package/edition/dispatched-runtime-components.mjs +82 -0
- package/edition/dist-check.mjs +1 -0
- package/edition/distribution-evidence-set.mjs +32 -5
- package/edition/edition-channel.mjs +15 -1
- package/edition/edition-locks.mjs +16 -6
- package/edition/edition-protocol.mjs +30 -8
- package/edition/edition-workspace.mjs +8 -0
- package/edition/external-source-walk.mjs +1 -1
- package/edition/image-evidence.mjs +134 -26
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +192 -4
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/operation-grant-admission.mjs +70 -0
- package/edition/operation-requirements.mjs +128 -0
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- package/edition/platform-lock-readability.mjs +2 -2
- package/edition/platform-service-class.mjs +42 -0
- package/edition/platform-service-ownership.mjs +78 -0
- package/edition/produce-distribution-completeness-evidence.mjs +5 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
- package/edition/release-catalogue.mjs +10 -3
- package/edition/remedy-command.mjs +2 -2
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +33 -34
- package/edition/resolve-distribution.mjs +70 -16
- package/edition/resolve-edition.mjs +3 -2
- package/edition/resolve-images.mjs +65 -4
- package/edition/seal-image-digests.mjs +17 -5
- package/edition/trust-policy-arming-verb.mjs +2 -10
- package/edition/verify-image-evidence.mjs +1 -0
- package/examples/README.md +3 -2
- 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:
|
|
35
|
-
contentDigest: '
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
1128
|
-
* `
|
|
1129
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1136
|
-
if (
|
|
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
|
|
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({
|
|
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>/
|
|
1161
|
-
* `<id>/
|
|
1162
|
-
*
|
|
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
|
|
1281
|
+
* checkout so the files it reads are HEAD-bound.
|
|
1165
1282
|
*/
|
|
1166
|
-
export async function
|
|
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
|
+
}
|