@xemahq/xema 0.51.0 → 0.52.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.
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+ // assemble-third-party-input — the producer input of ONE image of a THIRD-PARTY biome, assembled in the publisher's own
3
+ // GitHub-hosted workflow (XEMA-1611 design section 5).
4
+ //
5
+ // xema edition evidence assemble-third-party-input \
6
+ // --bundle <oci repository>@sha256:<64-hex> \
7
+ // --contract-authority <file> \
8
+ // --image <image repository>@sha256:<64-hex> \
9
+ // --subject-id <component image name> \
10
+ // --provenance <absolute exported BuildKit SLSA v1 predicate> \
11
+ // --registry-credentials <file> \
12
+ // --output <path> \
13
+ // [--ttl-seconds <n>]
14
+ //
15
+ // It runs on PUBLIC inputs only: this CLI and the `@xemahq/contracts` it ships (both from public npm), the publisher's
16
+ // own checkout at GITHUB_WORKSPACE, its own registry credential declaration and the GITHUB_* context of the run. It
17
+ // takes no data root, no kernel checkout and nothing of the vendor's. The checks that need the vendor's trust policy
18
+ // (the producer TTL ceiling, the signer rows, image ownership) run at the vendor's admission, by the trusted writer
19
+ // (`index runtime-image-source --trusted-write --third-party`), never here.
20
+ //
21
+ // Emits the EXACT producer input `produce-image-source-evidence.mjs` accepts, through the first-party assembler's own
22
+ // document builder and gates (`assemble-producer-input.mjs`). Field derivation:
23
+ // * contractAuthority — the `--contract-authority` file (the kernel contract authority the vendor's evidence trust
24
+ // pin names), proved to be the kernel this CLI ships before anything else is read.
25
+ // * subject kind and id — the bundle's `xema-biome.json` (`--bundle`, read BY DIGEST): the one service or worker
26
+ // component with an `oci-image` artifact whose image name is `--subject-id` (a service by
27
+ // `protocol.serviceName`, a worker by its key), kind `runtime-component` for a
28
+ // `scheduler: runtime` component and `service` otherwise, as the index builder names it.
29
+ // * repo, repositoryUrl, sourceCommit, producerWorkflowSha, signer, builderId, runId, runAttempt
30
+ // — GITHUB_REPOSITORY, GITHUB_SHA, GITHUB_WORKFLOW_REF, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT.
31
+ // * sourceManifestHash — the kernel hash of the bundle's `xema-biome.json`.
32
+ // * path — `<repo>/<biome dir>`: the directory of the ONE `xema-biome.json` of the checkout at
33
+ // GITHUB_SHA whose hash is the bundle's (none, or more than one, is refused by name).
34
+ // * sourceTreeHash — `hashImageBuildInputs` over the publisher's checkout at GITHUB_SHA, the same algorithm
35
+ // first-party images are hashed with, with no cross-repo inputs. It is publisher-attested:
36
+ // the vendor never recomputes it.
37
+ // * builder digests — the exported BuildKit SLSA v1 predicate (`--provenance`), whose VCS revision must be
38
+ // GITHUB_SHA.
39
+ // * manifest media type, platforms, sbomDigest, provenanceDigest
40
+ // — the registry, read BY DIGEST at `--image` (no tag is read), with the publisher's credential.
41
+ // * issuedAt/expiresAt — now, plus `--ttl-seconds` or DEFAULT_PRODUCER_TTL_SECONDS (72h). The ceiling is the vendor
42
+ // trust policy's maxProducerTtlSeconds, enforced by the vendor's third-party pin writer
43
+ // (`index runtime-image-source --trusted-write --third-party`) and `verify-evidence`; this CLI holds
44
+ // no second one. A value that is not a positive integer, or whose expiresAt RFC 3339 cannot
45
+ // name, is refused by name.
46
+ //
47
+ // It refuses to run unless RUNNER_ENVIRONMENT is `github-hosted` and GITHUB_REF is `refs/heads/main`. Those variables are
48
+ // self-asserted by the run, so this is only a fail-fast mirror: the authority is the vendor's admission, which checks
49
+ // the Fulcio certificate's Runner Environment and Source Repository Ref extensions (a third-party source is admitted
50
+ // only from the release channel of a GitHub-hosted workflow).
51
+
52
+ import { isAbsolute, resolve } from 'node:path';
53
+
54
+ import {
55
+ ProducerTtlCeilingSource,
56
+ assertAssembledProducerInput,
57
+ assertCanonicalSubjectId,
58
+ assertWorkflowContext,
59
+ deriveBuilder,
60
+ producerInputDocument,
61
+ producerTtlSecondsFlag,
62
+ readAssemblerFlags,
63
+ resolveImageArtifactEvidenceByDigest,
64
+ resolveProducerTtlSeconds,
65
+ writeProducerInputFile,
66
+ } from './assemble-producer-input.mjs';
67
+ import { configureEditionWorkspaceFromArgv, isEntryModule, kernelPackageDir, stateKernelPackageDir } from './edition-workspace.mjs';
68
+ import { readPublicContractAuthority } from './evidence-input-mode.mjs';
69
+ import { shippedKernelPackageDir } from './kernel-package.mjs';
70
+ import {
71
+ assertGitCommitExists,
72
+ biomeImageComponents,
73
+ claimBiomeImageName,
74
+ hashImageBuildInputs,
75
+ isRuntimeImageComponent,
76
+ imageInputSourceHashAlgorithm,
77
+ readGitJsonAtCommit,
78
+ } from './lib.mjs';
79
+ import { OrasRegistry } from './oras.mjs';
80
+ import { sandboxedGit } from './produce-image-source-evidence.mjs';
81
+ import { loadRegistryCredentialDeclarationsFile, resolveRegistryCredential, splitImageRef } from './registry-credentials.mjs';
82
+ import { subjectServicePath } from './resolve-images.mjs';
83
+ import { fetchBundleManifest } from './third-party-bundle-registry.mjs';
84
+ import { OCI_REPOSITORY_AT_DIGEST } from './third-party-source-flags.mjs';
85
+ import { PUBLISHER_ATTESTED_LABEL } from './third-party-subjects.mjs';
86
+
87
+ /** The runner a third-party producer input is assembled on. */
88
+ export const THIRD_PARTY_RUNNER_ENVIRONMENT = 'github-hosted';
89
+ /** The only ref a third-party producer input is assembled from (the release channel). */
90
+ export const THIRD_PARTY_PRODUCER_REF = 'refs/heads/main';
91
+
92
+ const BIOME_MANIFEST_FILE = 'xema-biome.json';
93
+ const OCI_IMAGE_ARTIFACT = 'oci-image';
94
+
95
+ /** The flags of the verb; every one but `--ttl-seconds` is required. */
96
+ export const THIRD_PARTY_INPUT_FLAGS = Object.freeze({
97
+ bundle: 'bundle',
98
+ contractAuthority: 'contract-authority',
99
+ image: 'image',
100
+ subjectId: 'subject-id',
101
+ provenance: 'provenance',
102
+ registryCredentials: 'registry-credentials',
103
+ output: 'output',
104
+ ttlSeconds: 'ttl-seconds',
105
+ });
106
+
107
+ export const THIRD_PARTY_INPUT_USAGE =
108
+ 'usage: xema edition evidence assemble-third-party-input --bundle <oci repository>@sha256:<64-hex> ' +
109
+ '--contract-authority <file> --image <image repository>@sha256:<64-hex> --subject-id <component image name> ' +
110
+ '--provenance <absolute exported SLSA v1 predicate> --registry-credentials <file> --output <path> [--ttl-seconds <n>]';
111
+
112
+ /**
113
+ * Refuse a run that is not a GitHub-hosted run of refs/heads/main, before anything else is read. RUNNER_ENVIRONMENT
114
+ * and GITHUB_REF are self-asserted, so this is a fail-fast mirror only; the authority is the vendor admission's check
115
+ * of the Fulcio certificate's Runner Environment and Source Repository Ref extensions.
116
+ */
117
+ export function assertThirdPartyRunner(env) {
118
+ if (env.RUNNER_ENVIRONMENT !== THIRD_PARTY_RUNNER_ENVIRONMENT) {
119
+ throw new Error(
120
+ `a third-party producer input is assembled only on a ${THIRD_PARTY_RUNNER_ENVIRONMENT} runner ` +
121
+ `(RUNNER_ENVIRONMENT is ${JSON.stringify(env.RUNNER_ENVIRONMENT ?? null)}): the vendor admits only certificates whose ` +
122
+ 'Fulcio Runner Environment extension is github-hosted',
123
+ );
124
+ }
125
+ if (env.GITHUB_REF !== THIRD_PARTY_PRODUCER_REF) {
126
+ throw new Error(
127
+ `a third-party producer input is assembled only from ${THIRD_PARTY_PRODUCER_REF} ` +
128
+ `(GITHUB_REF is ${JSON.stringify(env.GITHUB_REF ?? null)}): the vendor admits only certificates whose Fulcio ` +
129
+ `Source Repository Ref extension is ${THIRD_PARTY_PRODUCER_REF}, the release channel`,
130
+ );
131
+ }
132
+ }
133
+
134
+ function ociReference(flag, value) {
135
+ if (!OCI_REPOSITORY_AT_DIGEST.test(value ?? '')) {
136
+ throw new Error(`--${flag} must be <repository>@sha256:<64-hex>; got ${JSON.stringify(value)}`);
137
+ }
138
+ const at = value.indexOf('@');
139
+ return { repository: value.slice(0, at), digest: value.slice(at + 1) };
140
+ }
141
+
142
+ /**
143
+ * The bundle component `subjectId` names, and its runtime image subject kind, by the index builder's own rule
144
+ * (`biomeImageComponents`, `claimBiomeImageName`, `isRuntimeImageComponent` in lib.mjs): every image component is
145
+ * named, so a component with no image name, a web biome with an image and a duplicate image name are refused as the
146
+ * index builder refuses them; the subject is the one with its own `oci-image` artifact named `subjectId`.
147
+ */
148
+ export function bundleImageSubject({ manifest, subjectId, enums, bundleRef }) {
149
+ const xema = manifest?.xema;
150
+ const components = Array.isArray(xema?.components) ? xema.components : [];
151
+ const rel = `bundle ${bundleRef} ${BIOME_MANIFEST_FILE}`;
152
+ const taken = new Set();
153
+ const images = biomeImageComponents(components)
154
+ .map((component) => ({ component, name: claimBiomeImageName(component, { rel, id: xema?.id, target: xema?.target, taken }) }))
155
+ .filter(({ component }) => component.artifact?.kind === OCI_IMAGE_ARTIFACT);
156
+ const match = images.find(({ name }) => name === subjectId);
157
+ if (match === undefined) {
158
+ throw new Error(
159
+ `bundle ${bundleRef} has no ${OCI_IMAGE_ARTIFACT} service or worker component named ${JSON.stringify(subjectId)} ` +
160
+ `(its images: ${images.map(({ name }) => name).join(', ') || 'none'}); --subject-id names exactly one`,
161
+ );
162
+ }
163
+ const kind = isRuntimeImageComponent(match.component)
164
+ ? enums.RuntimeImageSubjectKind.RuntimeComponent
165
+ : enums.RuntimeImageSubjectKind.Service;
166
+ return { kind, id: subjectId };
167
+ }
168
+
169
+ /**
170
+ * The biome directory of the publisher's checkout at `commit`: the directory of the ONE `xema-biome.json` whose kernel
171
+ * source manifest hash is `sourceManifestHash` (`''` for the repository root). None, or more than one, is refused.
172
+ */
173
+ export async function biomeDirectoryAtCommit({ checkout, commit, sourceManifestHash, schemas, bundleRef }) {
174
+ const paths = sandboxedGit(checkout, ['ls-tree', '-r', '-z', '--name-only', commit])
175
+ .split('\0')
176
+ .filter((path) => path === BIOME_MANIFEST_FILE || path.endsWith(`/${BIOME_MANIFEST_FILE}`));
177
+ const matching = [];
178
+ for (const path of paths) {
179
+ const hash = await schemas.computeBiomeSourceManifestHash(readGitJsonAtCommit(checkout, path, commit));
180
+ if (hash === sourceManifestHash) {matching.push(path);}
181
+ }
182
+ if (matching.length !== 1) {
183
+ throw new Error(
184
+ `the checkout at ${commit} has ${matching.length} ${BIOME_MANIFEST_FILE} files whose source manifest hash is ` +
185
+ `${sourceManifestHash}, the hash of bundle ${bundleRef} ` +
186
+ `(${matching.length === 0 ? `its ${BIOME_MANIFEST_FILE} files: ${paths.join(', ') || 'none'}` : matching.join(', ')}); exactly one is required: ` +
187
+ 'the bundle must be published from this commit',
188
+ );
189
+ }
190
+ const [path] = matching;
191
+ return path === BIOME_MANIFEST_FILE ? '' : path.slice(0, -`/${BIOME_MANIFEST_FILE}`.length);
192
+ }
193
+
194
+ /**
195
+ * Assemble the producer input of one third-party image and prove it satisfies the producer's exact contract.
196
+ *
197
+ * @param {object} request
198
+ * @param {string} request.bundle `<oci repository>@sha256:<hex>` of the published biome bundle
199
+ * @param {string} request.contractAuthorityPath the `--contract-authority` file
200
+ * @param {string} request.image `<image repository>@sha256:<hex>` of the published image
201
+ * @param {string} request.subjectId the bundle component's image name
202
+ * @param {string} request.provenancePath the exported BuildKit SLSA v1 predicate (absolute)
203
+ * @param {string} request.registryCredentialsPath the publisher's registry credential declaration file
204
+ * @param {number} [request.ttlSeconds]
205
+ * @param {string} [request.kernelDir] the kernel package (default: the configured Kernel root)
206
+ * @param {object} [request.env]
207
+ * @param {Function} [request.registryFor] `({authority, credential}) => client`, for tests
208
+ * @param {number} [request.now]
209
+ */
210
+ export async function assembleThirdPartyInput({
211
+ bundle,
212
+ contractAuthorityPath,
213
+ image,
214
+ subjectId,
215
+ provenancePath,
216
+ registryCredentialsPath,
217
+ ttlSeconds,
218
+ kernelDir = kernelPackageDir(),
219
+ env = process.env,
220
+ fetchImpl = globalThis.fetch,
221
+ registryFor = null,
222
+ now = Date.now(),
223
+ }) {
224
+ assertThirdPartyRunner(env);
225
+ const context = assertWorkflowContext(env);
226
+ ociReference(THIRD_PARTY_INPUT_FLAGS.bundle, bundle);
227
+ const imageRef = ociReference(THIRD_PARTY_INPUT_FLAGS.image, image);
228
+ assertCanonicalSubjectId(subjectId);
229
+ const effectiveTtlSeconds = resolveProducerTtlSeconds({ ttlSeconds, now, ceiling: { source: ProducerTtlCeilingSource.VendorAdmission } });
230
+ const checkout = env.GITHUB_WORKSPACE;
231
+ if (typeof checkout !== 'string' || !isAbsolute(checkout)) {
232
+ throw new Error(
233
+ `GITHUB_WORKSPACE (${JSON.stringify(checkout ?? null)}) is not an absolute path: the publisher's checkout at GITHUB_SHA is hashed there`,
234
+ );
235
+ }
236
+ try {
237
+ assertGitCommitExists(checkout, context.sha);
238
+ } catch (error) {
239
+ throw new Error(`the publisher's checkout at GITHUB_WORKSPACE ${checkout} does not hold GITHUB_SHA ${context.sha}: ${error.message}`);
240
+ }
241
+ const { contractAuthority, schemas } = await readPublicContractAuthority({ path: contractAuthorityPath, kernelDir });
242
+ const builder = deriveBuilder({ provenancePath, sourceCommit: context.sha });
243
+
244
+ const declarations = loadRegistryCredentialDeclarationsFile(registryCredentialsPath);
245
+ const clients = new Map();
246
+ const clientFor = (authority) => {
247
+ if (!clients.has(authority)) {
248
+ const credential = resolveRegistryCredential({ authority, declarations, env });
249
+ // No vendor data root: the publisher's oras is held to its version, not to a vendor pin. Every byte read is
250
+ // content-addressed here and re-verified by the vendor's admission.
251
+ clients.set(authority, registryFor ? registryFor({ authority, credential }) : new OrasRegistry({ credential, trust: () => null, fetchImpl }));
252
+ }
253
+ return clients.get(authority);
254
+ };
255
+
256
+ const { manifest } = await fetchBundleManifest({ clientFor, ociRef: bundle });
257
+ const sourceManifestHash = await schemas.computeBiomeSourceManifestHash(manifest);
258
+ const subject = bundleImageSubject({ manifest, subjectId, enums: schemas, bundleRef: bundle });
259
+ const biomeDirectory = await biomeDirectoryAtCommit({ checkout, commit: context.sha, sourceManifestHash, schemas, bundleRef: bundle });
260
+ const servicePath = subjectServicePath(
261
+ { kind: subject.kind, id: subject.id, repo: context.repository, path: biomeDirectory === '' ? '.' : biomeDirectory },
262
+ context.sha,
263
+ () => ({ dir: checkout, base: checkout }),
264
+ );
265
+ const sourceTreeHash = hashImageBuildInputs(
266
+ { repoDir: checkout, service: subject.id, servicePath, commit: context.sha, crossRepoInputs: [], buildStepDirectories: [] },
267
+ imageInputSourceHashAlgorithm(),
268
+ );
269
+ const artifact = await resolveImageArtifactEvidenceByDigest({
270
+ client: clientFor(splitImageRef(imageRef.repository).authority),
271
+ imageRepository: imageRef.repository,
272
+ manifestDigest: imageRef.digest,
273
+ referenceProvenance: builder.predicate,
274
+ });
275
+ const input = producerInputDocument({
276
+ contractAuthority,
277
+ context,
278
+ builder,
279
+ now,
280
+ ttlSeconds: effectiveTtlSeconds,
281
+ subject: {
282
+ kind: subject.kind,
283
+ id: subject.id,
284
+ repositoryUrl: `https://github.com/${context.repository}`,
285
+ repo: context.repository,
286
+ path: biomeDirectory === '' ? context.repository : `${context.repository}/${biomeDirectory}`,
287
+ sourceCommit: context.sha,
288
+ sourceManifestHash,
289
+ sourceTreeHash,
290
+ imageRepository: imageRef.repository,
291
+ ...artifact,
292
+ },
293
+ });
294
+ assertAssembledProducerInput({ input, schemas, contractAuthority, context });
295
+ return input;
296
+ }
297
+
298
+ function requireFlag(flags, name) {
299
+ const value = flags.get(name);
300
+ if (value === undefined) {
301
+ throw new Error(`--${name} is required\n${THIRD_PARTY_INPUT_USAGE}`);
302
+ }
303
+ return value;
304
+ }
305
+
306
+ async function main() {
307
+ // No workspace root: the verb runs on public inputs, with the kernel this CLI ships.
308
+ const args = configureEditionWorkspaceFromArgv(process.argv.slice(2), [], { refuseOthers: true });
309
+ stateKernelPackageDir(shippedKernelPackageDir());
310
+ const flags = readAssemblerFlags(args, new Set(Object.values(THIRD_PARTY_INPUT_FLAGS)));
311
+ const output = resolve(requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.output));
312
+ const ttl = flags.get(THIRD_PARTY_INPUT_FLAGS.ttlSeconds);
313
+ const input = await assembleThirdPartyInput({
314
+ bundle: requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.bundle),
315
+ contractAuthorityPath: resolve(requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.contractAuthority)),
316
+ image: requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.image),
317
+ subjectId: requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.subjectId),
318
+ provenancePath: requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.provenance),
319
+ registryCredentialsPath: resolve(requireFlag(flags, THIRD_PARTY_INPUT_FLAGS.registryCredentials)),
320
+ ttlSeconds: producerTtlSecondsFlag(ttl),
321
+ });
322
+ writeProducerInputFile(output, input);
323
+ const [subject] = input.subjects;
324
+ console.log(
325
+ `[assemble-third-party-input] wrote ${output} for subject ${subject.kind}:${subject.id} ` +
326
+ `(${subject.imageRepository}@${subject.manifestDigest}, ${subject.repo}@${subject.sourceCommit}, ` +
327
+ `sourceTreeHash (${PUBLISHER_ATTESTED_LABEL}) ${subject.sourceTreeHash})`,
328
+ );
329
+ }
330
+
331
+ if (isEntryModule(import.meta.url)) {
332
+ main().catch((error) => {
333
+ console.error(`[assemble-third-party-input] ${error.message}`);
334
+ process.exitCode = 1;
335
+ });
336
+ }
@@ -0,0 +1,154 @@
1
+ // evidence-input-mode — where a producer-side evidence verb takes its kernel contract authority and schemas from.
2
+ //
3
+ // Vendor mode (`--data-root <dir> --kernel <dir>`): the authority the data root's evidence-trust pin records, and the
4
+ // schemas of a kernel checkout at that commit. Public-input mode (`--contract-authority <file>`): a third-party
5
+ // publisher's run, on public inputs only. The authority is the file, and the schemas are the `@xemahq/contracts` this
6
+ // CLI bundles, proved to be that authority. There is no data root, no kernel checkout and no vendor trust policy; what
7
+ // needs the vendor's trust policy is checked at the vendor's admission.
8
+ //
9
+ // `readPublicContractAuthority` is the one resolution of a `--contract-authority` file, shared by
10
+ // `evidence assemble-third-party-input`, `statement producer <stage|accept>` and `oci <attach-plan|validate>`.
11
+
12
+ import { readFileSync } from 'node:fs';
13
+ import { join, resolve } from 'node:path';
14
+ import { pathToFileURL } from 'node:url';
15
+
16
+ import { readKernelEditionProtocol } from './edition-protocol.mjs';
17
+ import {
18
+ WorkspaceRoot,
19
+ configureEditionWorkspaceFromArgv,
20
+ kernelPackageDir,
21
+ stateKernelPackageDir,
22
+ } from './edition-workspace.mjs';
23
+ import { KERNEL_PACKAGE_NAME, assertKernelPackageIsCatalogueAuthority, shippedKernelPackageDir } from './kernel-package.mjs';
24
+ import { loadKernelDistributionSchemas } from './lib.mjs';
25
+
26
+ /** The two closed ways a producer-side evidence verb is given its kernel contract authority. */
27
+ export const EvidenceInputMode = Object.freeze({
28
+ /** `--data-root` and `--kernel`: the vendor's pinned authority and kernel checkout. */
29
+ Vendor: 'vendor',
30
+ /** `--contract-authority`: a publisher's run on public inputs, with the kernel this CLI bundles. */
31
+ PublicInput: 'public-input',
32
+ });
33
+
34
+ const CONTRACT_AUTHORITY_FLAG = '--contract-authority';
35
+ const VENDOR_FLAGS = ['--data-root', '--kernel'];
36
+
37
+ const flagOf = (arg) => {
38
+ const eq = typeof arg === 'string' ? arg.indexOf('=') : -1;
39
+ return eq > 0 ? arg.slice(0, eq) : arg;
40
+ };
41
+
42
+ const readVersion = (packageJsonPath) => {
43
+ const { version } = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
44
+ if (typeof version !== 'string' || version.length === 0) {throw new Error(`${packageJsonPath} has no "version"`);}
45
+ return version;
46
+ };
47
+
48
+ /**
49
+ * The bundled kernel's own strict `KernelContractAuthoritySchema` (not re-exported by its distribution index), read
50
+ * from the package this CLI ships.
51
+ */
52
+ async function loadKernelContractAuthoritySchema(kernelDir) {
53
+ const module = join(kernelDir, 'dist', 'distribution', 'lib', 'kernel-contract-authority.js');
54
+ let loaded;
55
+ try {
56
+ loaded = await import(pathToFileURL(module).href);
57
+ } catch (error) {
58
+ throw new Error(`${KERNEL_PACKAGE_NAME} at ${kernelDir} has no loadable kernel contract authority schema (${module}): ${error.message}`);
59
+ }
60
+ const schema = loaded.KernelContractAuthoritySchema ?? loaded.default?.KernelContractAuthoritySchema;
61
+ if (typeof schema?.safeParse !== 'function') {
62
+ throw new Error(`${KERNEL_PACKAGE_NAME} at ${kernelDir} exports no KernelContractAuthoritySchema from ${module}`);
63
+ }
64
+ return schema;
65
+ }
66
+
67
+ /**
68
+ * The contract authority of a `--contract-authority` file, proved to be the kernel this CLI bundles (`kernelDir`),
69
+ * and that kernel's distribution schemas. An authority whose contracts digest is not the bundled one is refused
70
+ * first, naming both digests: the publisher installs the xema CLI whose bundled `@xemahq/contracts` is that authority.
71
+ * The file is then strict-parsed by the kernel's `KernelContractAuthoritySchema` (an unknown or missing field is
72
+ * refused) and returned in the schema's form, so its key order is never what a later comparison sees.
73
+ *
74
+ * `sourceCommit` is held to the schema's shape only: it is provenance, which the bundled package cannot prove. The
75
+ * vendor holds it, comparing the statement's whole authority with its pinned one at the third-party pin writer
76
+ * (`assertImageStatementBindsPin`, third-party-source-evidence.mjs).
77
+ */
78
+ export async function readPublicContractAuthority({ path, kernelDir = kernelPackageDir() }) {
79
+ let contractAuthority;
80
+ try {
81
+ contractAuthority = JSON.parse(readFileSync(path, 'utf8'));
82
+ } catch (error) {
83
+ throw new Error(`${CONTRACT_AUTHORITY_FLAG} ${path} is not a JSON kernel contract authority: ${error.message}`);
84
+ }
85
+ if (typeof contractAuthority !== 'object' || contractAuthority === null || Array.isArray(contractAuthority)) {
86
+ throw new Error(`${CONTRACT_AUTHORITY_FLAG} ${path} does not hold one kernel contract authority object`);
87
+ }
88
+ const bundledDigest = readKernelEditionProtocol(kernelDir).contentDigest;
89
+ if (contractAuthority.contentDigest !== bundledDigest) {
90
+ const cli = readVersion(new URL('../package.json', import.meta.url));
91
+ const contracts = readVersion(join(kernelDir, 'package.json'));
92
+ throw new Error(
93
+ `${CONTRACT_AUTHORITY_FLAG} ${path} is kernel contracts digest ${JSON.stringify(contractAuthority.contentDigest)}, ` +
94
+ `but this CLI (@xemahq/xema ${cli}) bundles ${KERNEL_PACKAGE_NAME} ${contracts}, digest ${bundledDigest}; ` +
95
+ `install the @xemahq/xema version whose bundled ${KERNEL_PACKAGE_NAME} has digest ${JSON.stringify(contractAuthority.contentDigest)}`,
96
+ );
97
+ }
98
+ const parsed = (await loadKernelContractAuthoritySchema(kernelDir)).safeParse(contractAuthority);
99
+ if (!parsed.success) {
100
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`).join('; ');
101
+ throw new Error(`${CONTRACT_AUTHORITY_FLAG} ${path} is not a kernel contract authority: ${issues}`);
102
+ }
103
+ contractAuthority = parsed.data;
104
+ try {
105
+ const schemas = await assertKernelPackageIsCatalogueAuthority({
106
+ catalogue: { contractAuthority },
107
+ kernelPackageDir: kernelDir,
108
+ loadSchemas: loadKernelDistributionSchemas,
109
+ });
110
+ return { contractAuthority, schemas };
111
+ } catch (error) {
112
+ throw new Error(
113
+ `${CONTRACT_AUTHORITY_FLAG} ${path} is not the kernel this CLI ships (${kernelDir}): ${error.message}; ` +
114
+ `install the xema CLI version whose ${KERNEL_PACKAGE_NAME} is that authority`,
115
+ );
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Configure a producer-side evidence verb from its argv in exactly one mode. `--contract-authority <file>` together
121
+ * with `--data-root`/`--kernel`, or neither, is refused by name.
122
+ *
123
+ * Vendor mode configures the Data and Kernel roots and returns `{mode, rest}`; the verb reads its pinned authority.
124
+ * Public-input mode configures no root, states the bundled kernel as the Kernel root and returns
125
+ * `{mode, rest, contractAuthority, schemas}` from {@link readPublicContractAuthority}.
126
+ */
127
+ export async function configureEvidenceInputMode(argv, { verb }) {
128
+ const authorityAt = argv.findIndex((arg) => flagOf(arg) === CONTRACT_AUTHORITY_FLAG);
129
+ const vendorFlags = VENDOR_FLAGS.filter((flag) => argv.some((arg) => flagOf(arg) === flag));
130
+ const modes = `${CONTRACT_AUTHORITY_FLAG} <file> (public-input mode) or --data-root <dir> --kernel <dir> (vendor mode)`;
131
+ if (authorityAt !== -1 && vendorFlags.length > 0) {
132
+ throw new Error(`${verb}: ${CONTRACT_AUTHORITY_FLAG} and ${vendorFlags.join('/')} are two modes; pass one: ${modes}`);
133
+ }
134
+ if (authorityAt === -1 && vendorFlags.length === 0) {
135
+ throw new Error(`${verb} needs ${modes}`);
136
+ }
137
+ if (authorityAt === -1) {
138
+ return { mode: EvidenceInputMode.Vendor, rest: configureEditionWorkspaceFromArgv(argv, [WorkspaceRoot.Data, WorkspaceRoot.Kernel]) };
139
+ }
140
+ const arg = argv[authorityAt];
141
+ const inline = arg.length > CONTRACT_AUTHORITY_FLAG.length;
142
+ const path = inline ? arg.slice(CONTRACT_AUTHORITY_FLAG.length + 1) : argv[authorityAt + 1];
143
+ if (typeof path !== 'string' || path.length === 0 || path.startsWith('--')) {
144
+ throw new Error(`${CONTRACT_AUTHORITY_FLAG} requires a file path`);
145
+ }
146
+ if (argv.some((other, index) => index !== authorityAt && flagOf(other) === CONTRACT_AUTHORITY_FLAG)) {
147
+ throw new Error(`${CONTRACT_AUTHORITY_FLAG} is given more than once`);
148
+ }
149
+ const remaining = argv.filter((_, index) => index !== authorityAt && (inline || index !== authorityAt + 1));
150
+ const rest = configureEditionWorkspaceFromArgv(remaining, [], { refuseOthers: true });
151
+ stateKernelPackageDir(shippedKernelPackageDir());
152
+ const { contractAuthority, schemas } = await readPublicContractAuthority({ path: resolve(path) });
153
+ return { mode: EvidenceInputMode.PublicInput, rest, contractAuthority, schemas };
154
+ }
@@ -138,11 +138,29 @@ function assertTimeWindow({ issuedAt, expiresAt, now, maxTtlSeconds, label }) {
138
138
  if (expires <= instant) {
139
139
  throw new Error(`${label} is expired`);
140
140
  }
141
+ assertProducerTtlCeiling({ issuedAt, expiresAt, maxTtlSeconds, label });
142
+ }
143
+
144
+ /**
145
+ * The declared lifetime (`expiresAt - issuedAt`) is at most the trust policy's ceiling `maxTtlSeconds`. Held by
146
+ * `verify-evidence` (through the time window above) and by the third-party pin writer, which has no `now` to hold.
147
+ */
148
+ export function assertProducerTtlCeiling({ issuedAt, expiresAt, maxTtlSeconds, label }) {
149
+ if (!Number.isSafeInteger(maxTtlSeconds) || maxTtlSeconds < 1) {
150
+ throw new Error(`${label}: the trust policy's TTL ceiling ${String(maxTtlSeconds)} is not a positive integer`);
151
+ }
152
+ const issued = Date.parse(issuedAt);
153
+ const expires = Date.parse(expiresAt);
154
+ if (![issued, expires].every(Number.isFinite)) {
155
+ throw new Error(`${label} has an invalid time`);
156
+ }
141
157
  if (expires <= issued) {
142
158
  throw new Error(`${label} expiresAt must be after issuedAt`);
143
159
  }
144
160
  if (expires - issued > maxTtlSeconds * 1000) {
145
- throw new Error(`${label} exceeds the trusted maximum TTL`);
161
+ throw new Error(
162
+ `${label} exceeds the trusted maximum TTL (${(expires - issued) / 1000}s declared, maxProducerTtlSeconds ${maxTtlSeconds})`,
163
+ );
146
164
  }
147
165
  }
148
166
 
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import { lstatSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
 
5
+ import { canonicalJson } from './canonical-evidence-json.mjs';
5
6
  import { kernelRepoDir } from './edition-workspace.mjs';
6
7
  import { COMMIT_PATTERN } from './evidence-grammars.mjs';
7
8
  import {
@@ -198,12 +199,19 @@ export function assertSourcePathsMatchCommit(repositoryDirectory, sourceCommit,
198
199
  assertSourcePathsEqualSet(sourcePaths, sourcePathSetAtCommit(repositoryDirectory, sourceCommit), sourceCommit);
199
200
  }
200
201
 
202
+ /** The canonical JSON of an authority (sorted keys), or null for a value canonical evidence JSON cannot hold. */
201
203
  function exactAuthorityJson(value) {
202
- return JSON.stringify(value);
204
+ try {
205
+ return canonicalJson(value);
206
+ } catch {
207
+ return null;
208
+ }
203
209
  }
204
210
 
211
+ /** `actual` is exactly `expected`, field for field and value for value; key order is not part of an authority. */
205
212
  export function assertKernelContractAuthorityEqual(actual, expected, label) {
206
- if (exactAuthorityJson(actual) !== exactAuthorityJson(expected)) {
213
+ const canonical = exactAuthorityJson(actual);
214
+ if (canonical === null || canonical !== exactAuthorityJson(expected)) {
207
215
  throw new Error(
208
216
  `${label} does not exactly match kernel contract authority`,
209
217
  );
@@ -87,6 +87,19 @@ function shippedPackageDir(name) {
87
87
  return undefined;
88
88
  }
89
89
 
90
+ /**
91
+ * The `@xemahq/contracts` package this CLI itself ships (installed from public npm with the CLI): the kernel of a
92
+ * verb that runs with no kernel checkout and no catalogue, such as a third-party publisher's
93
+ * `evidence assemble-third-party-input`. Refused when the CLI's installation has none.
94
+ */
95
+ export function shippedKernelPackageDir() {
96
+ const dir = shippedPackageDir(KERNEL_PACKAGE_NAME);
97
+ if (dir === undefined) {
98
+ throw new KernelPackageError(`this CLI's installation ships no ${KERNEL_PACKAGE_NAME}; reinstall the CLI from npm`);
99
+ }
100
+ return dir;
101
+ }
102
+
90
103
  function linkRuntimeDependencies(packageDir) {
91
104
  const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
92
105
  const nodeModules = dirname(manifest.name.startsWith('@') ? dirname(packageDir) : packageDir);