@xemahq/xema 0.48.0 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/appliance-base/distribution.lock.json +3936 -1515
- package/dist/assets/appliance-base/images.lock.json +878 -1161
- package/dist/assets/appliance-lean/distribution.lock.json +3663 -916
- package/dist/assets/appliance-lean/images.lock.json +825 -924
- package/dist/assets/boot-graph.json +398 -93
- package/dist/assets/distribution-assets.manifest.json +10 -10
- package/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +5 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +0 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +4386 -2503
- package/dist/edition/verify-image-evidence.bundle.provenance.json +590 -488
- package/dist/runtime/auth/cli-application.d.ts +18 -0
- package/dist/runtime/auth/cli-application.d.ts.map +1 -0
- package/dist/runtime/auth/cli-application.js +39 -0
- package/dist/runtime/auth/cli-application.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +3 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +5 -1
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +21 -0
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +0 -18
- package/dist/runtime/launch.js.map +1 -1
- package/edition/README.md +20 -3
- package/edition/biome-content.mjs +5 -2
- package/edition/build-runtime-image-source-index.mjs +79 -5
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/catalogue-evidence.mjs +8 -5
- package/edition/customer-edition-verbs.mjs +9 -3
- package/edition/develop-coverage-run.mjs +2 -1
- package/edition/develop-coverage.mjs +12 -1
- package/edition/develop-edition-resolve.mjs +3 -2
- package/edition/develop-edition.mjs +15 -3
- package/edition/develop-record-verify.mjs +216 -0
- package/edition/develop-select-records.mjs +24 -225
- package/edition/develop-verify-image-record.mjs +90 -0
- package/edition/dist-check.mjs +24 -3
- package/edition/distribution-evidence-set.mjs +30 -2
- package/edition/edition-artifact.mjs +110 -22
- package/edition/edition-ceiling.mjs +180 -0
- package/edition/edition-config.mjs +91 -0
- package/edition/edition-constants.mjs +21 -3
- package/edition/edition-contract-parse.mjs +88 -3
- package/edition/edition-contract.json +47 -4
- package/edition/edition-image-seal.mjs +9 -2
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-protocol.mjs +7 -1
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/evidence-grammars.mjs +2 -0
- package/edition/evidence-trust-pin.mjs +66 -10
- package/edition/evidence-trust-policy.mjs +39 -29
- package/edition/external-source-walk.mjs +16 -3
- package/edition/fleet-coordinate-finality.mjs +18 -5
- package/edition/fulcio-certificate-extensions.mjs +231 -0
- package/edition/image-evidence.mjs +34 -79
- package/edition/locked-bundle-signer.mjs +241 -0
- package/edition/oci-evidence-verbs.mjs +7 -1
- package/edition/pinned-cosign.mjs +70 -0
- package/edition/plan-image-evidence-refresh.mjs +3 -1
- package/edition/producer-evidence-bundle.mjs +3 -1
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/release-edition-artifact.mjs +3 -3
- package/edition/repin-evidence-trust.mjs +6 -1
- package/edition/resolve-distribution.mjs +90 -7
- package/edition/resolve-edition.mjs +1 -1
- package/edition/resolve-images.mjs +53 -7
- package/edition/seal-image-digests.mjs +19 -8
- package/edition/sealed-distribution-lock.mjs +2 -2
- package/edition/third-party-admission.mjs +91 -0
- package/edition/third-party-bundle-registry.mjs +147 -0
- package/edition/third-party-certificate-extensions.mjs +21 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +255 -0
- package/edition/third-party-source-flags.mjs +134 -0
- package/edition/third-party-source-pins.mjs +259 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +115 -0
- package/edition/third-party-source-writer.mjs +229 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/verify-catalogue-evidence.mjs +2 -0
- package/edition/verify-image-evidence.mjs +2 -0
- package/edition/verify-image-platforms.mjs +33 -9
- package/package.json +5 -5
- package/dist/runtime/auth/client-bootstrap.d.ts +0 -11
- package/dist/runtime/auth/client-bootstrap.d.ts.map +0 -1
- package/dist/runtime/auth/client-bootstrap.js +0 -60
- package/dist/runtime/auth/client-bootstrap.js.map +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// The trusted writer of a third-party source pin (XEMA-1611 design section 3): `xema edition index runtime-image-source
|
|
2
|
+
// --trusted-write --third-party <name> ...`. It PLANS the write: reads the bundle and the evidence from the registry,
|
|
3
|
+
// runs checks (a) to (e) on the exact bytes it would commit, and returns the files, the pin record and the candidate
|
|
4
|
+
// index. `commitThirdPartySourceWrite` then writes them. The third-party repository is never cloned.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { REPIN_COMMAND } from './evidence-trust-pin.mjs';
|
|
11
|
+
import { canonicalJsonBytes, evidenceDirectoryForDigest, policyCandidates, sha256Digest } from './image-evidence.mjs';
|
|
12
|
+
import { resolveImageSourceEvidenceReferrer } from './oci-image-source-evidence.mjs';
|
|
13
|
+
import { PRODUCER_EVIDENCE_INDEX_FILE } from './producer-evidence-index-file.mjs';
|
|
14
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
15
|
+
import { bundleImageNames, fetchBundleManifest, fetchBundleProvenance, verifyBundleOnRegistry } from './third-party-bundle-registry.mjs';
|
|
16
|
+
import {
|
|
17
|
+
PROVENANCE_BUNDLE_FILE,
|
|
18
|
+
PROVENANCE_STATEMENT_FILE,
|
|
19
|
+
SLSA_PROVENANCE_V1,
|
|
20
|
+
THIRD_PARTY_EVIDENCE_CHANNEL,
|
|
21
|
+
digestOfOciRef,
|
|
22
|
+
provenanceDirectoryForDigest,
|
|
23
|
+
thirdPartySignerRow,
|
|
24
|
+
verifyThirdPartySourceEvidence,
|
|
25
|
+
} from './third-party-source-evidence.mjs';
|
|
26
|
+
import { THIRD_PARTY_SOURCE_PINS_FILE, THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION, loadThirdPartySourcePins } from './third-party-source-pins.mjs';
|
|
27
|
+
import { assertThirdPartyUniqueness } from './third-party-source-uniqueness.mjs';
|
|
28
|
+
|
|
29
|
+
const compare = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
30
|
+
|
|
31
|
+
/** The one release-channel row that signs the biome bundle's provenance for `repository`. */
|
|
32
|
+
function bundleSignerRow(policy, repository) {
|
|
33
|
+
const rows = policy.policies.filter(
|
|
34
|
+
(row) =>
|
|
35
|
+
row.channel === THIRD_PARTY_EVIDENCE_CHANNEL &&
|
|
36
|
+
row.predicateTypes.includes(SLSA_PROVENANCE_V1) &&
|
|
37
|
+
row.githubWorkflow?.repository === repository,
|
|
38
|
+
);
|
|
39
|
+
if (rows.length !== 1) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`the trust policy has ${rows.length} ${THIRD_PARTY_EVIDENCE_CHANNEL} rows authorizing ${SLSA_PROVENANCE_V1} for ${repository}; exactly one bundle-signer row is required`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return rows[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The one release-channel row whose imageRepositories lists `imageRef`. */
|
|
48
|
+
function imageSignerRow(policy, schemas, imageRef) {
|
|
49
|
+
const rows = policyCandidates(policy, schemas.IMAGE_PRODUCER_PREDICATE_TYPE, THIRD_PARTY_EVIDENCE_CHANNEL, imageRef);
|
|
50
|
+
if (rows.length !== 1) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`${rows.length} ${THIRD_PARTY_EVIDENCE_CHANNEL} trust policy rows list ${imageRef} in their imageRepositories for image-source evidence; exactly one is required`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return rows[0];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function collectImage({ image, request, context, clientFor, services }) {
|
|
59
|
+
const { schemas, expectedContractAuthority, policy } = context;
|
|
60
|
+
const label = `image ${image.imageRef}@${image.manifestDigest}`;
|
|
61
|
+
const row = imageSignerRow(policy, schemas, image.imageRef);
|
|
62
|
+
const { authority } = splitImageRef(image.imageRef);
|
|
63
|
+
const skipped = [];
|
|
64
|
+
const evidence = await resolveImageSourceEvidenceReferrer({
|
|
65
|
+
client: clientFor(authority),
|
|
66
|
+
imageRepository: image.imageRef,
|
|
67
|
+
subjectDigest: image.manifestDigest,
|
|
68
|
+
schemas,
|
|
69
|
+
expectedContractAuthority,
|
|
70
|
+
onInvalidReferrer: (report) => skipped.push(`${report.artifactDigest}: ${report.reason}`),
|
|
71
|
+
});
|
|
72
|
+
if (evidence === null) {
|
|
73
|
+
throw new Error(`${label} has no valid image-source evidence referrer${skipped.length === 0 ? '' : ` (skipped: ${skipped.join('; ')})`}`);
|
|
74
|
+
}
|
|
75
|
+
if (evidence.records.length !== 1) {
|
|
76
|
+
throw new Error(`${label}: the evidence referrer carries ${evidence.records.length} statements; a third-party image has exactly one`);
|
|
77
|
+
}
|
|
78
|
+
const [record] = evidence.records;
|
|
79
|
+
const { source } = record.statement.predicate;
|
|
80
|
+
if (!services.has(source.id)) {
|
|
81
|
+
throw new Error(`${label}: its statement attests ${source.kind}:${source.id}, which is not an oci-image service of bundle ${request.bundleOciRef} (${[...services].join(', ')})`);
|
|
82
|
+
}
|
|
83
|
+
const directory = evidenceDirectoryForDigest(image.manifestDigest);
|
|
84
|
+
return {
|
|
85
|
+
pin: {
|
|
86
|
+
subjectKind: source.kind,
|
|
87
|
+
subjectId: source.id,
|
|
88
|
+
imageRef: image.imageRef,
|
|
89
|
+
manifestDigest: image.manifestDigest,
|
|
90
|
+
sourceTreeHash: source.sourceTreeHash,
|
|
91
|
+
sourceHashAlgorithm: source.sourceHashAlgorithm,
|
|
92
|
+
statementDigest: record.descriptor.statementDigest,
|
|
93
|
+
bundleDigest: record.descriptor.bundleDigest,
|
|
94
|
+
policyId: row.policyId,
|
|
95
|
+
},
|
|
96
|
+
files: [
|
|
97
|
+
[`${directory}/${PRODUCER_EVIDENCE_INDEX_FILE}`, evidence.indexBytes],
|
|
98
|
+
[`${directory}/${record.descriptor.statementPath}`, record.statementBytes],
|
|
99
|
+
[`${directory}/${record.descriptor.bundlePath}`, record.bundleBytes],
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Plan the write of one third-party source. Throws on the first violated check; writes nothing to the data root.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} options
|
|
108
|
+
* @param {{name, repository, repositoryId, repositoryOwnerId, sourceCommit, bundleOciRef, biomeId, images: {imageRef, manifestDigest}[]}} options.request
|
|
109
|
+
* @param {object} options.context `loadThirdPartySourceContext`
|
|
110
|
+
* @param {(authority: string) => object} options.clientFor the registry client of an authority
|
|
111
|
+
* @param {(request: {committedIndex: object, pin: {repository: string, workflowCommit: string}}) => object} options.withProducerWorkflowPin
|
|
112
|
+
* the committed runtime image source index with the pin merged in, built by the index generator
|
|
113
|
+
* @param {string} options.dataRoot
|
|
114
|
+
* @param {(options: object) => void} [options.verifyOnRegistry]
|
|
115
|
+
*/
|
|
116
|
+
export async function planThirdPartySourceWrite({ request, context, clientFor, withProducerWorkflowPin, dataRoot, verifyOnRegistry = verifyBundleOnRegistry }) {
|
|
117
|
+
const { policy, schemas } = context;
|
|
118
|
+
const repositoryUrl = `https://github.com/${request.repository}`;
|
|
119
|
+
const row = bundleSignerRow(policy, request.repository);
|
|
120
|
+
const workflowPath = row.githubWorkflow.path;
|
|
121
|
+
// The row must be exact (release channel, refs/heads/main, the GitHub issuer, its own workflow identity) before cosign runs.
|
|
122
|
+
thirdPartySignerRow({ policy, policyId: row.policyId, source: { repository: request.repository, producerWorkflow: { path: workflowPath } }, schemas, label: `third-party source "${request.name}" bundle signer` });
|
|
123
|
+
|
|
124
|
+
// (a) the bundle: the registry's signature and provenance under exactly this row, then its bytes.
|
|
125
|
+
verifyOnRegistry({
|
|
126
|
+
trustedCosign: context.trustedCosign,
|
|
127
|
+
policy,
|
|
128
|
+
policyRoot: context.policyRoot,
|
|
129
|
+
row,
|
|
130
|
+
workflowSha: request.sourceCommit,
|
|
131
|
+
ociRef: request.bundleOciRef,
|
|
132
|
+
});
|
|
133
|
+
const { manifest } = await fetchBundleManifest({ clientFor, ociRef: request.bundleOciRef });
|
|
134
|
+
if (manifest?.xema?.id !== request.biomeId) {
|
|
135
|
+
throw new Error(`bundle ${request.bundleOciRef} declares xema.id ${JSON.stringify(manifest?.xema?.id)}, but --biome-id is ${JSON.stringify(request.biomeId)}`);
|
|
136
|
+
}
|
|
137
|
+
const sourceManifestHash = await schemas.computeBiomeSourceManifestHash(manifest);
|
|
138
|
+
const services = bundleImageNames(manifest);
|
|
139
|
+
const provenance = await fetchBundleProvenance({ clientFor, ociRef: request.bundleOciRef });
|
|
140
|
+
const provenanceDirectory = provenanceDirectoryForDigest(digestOfOciRef(request.bundleOciRef));
|
|
141
|
+
|
|
142
|
+
// (b) each image's image-source/v2 evidence, as an OCI referrer of imageRef@digest.
|
|
143
|
+
const images = [];
|
|
144
|
+
const files = [
|
|
145
|
+
[`${provenanceDirectory}/${PROVENANCE_STATEMENT_FILE}`, provenance.statementBytes],
|
|
146
|
+
[`${provenanceDirectory}/${PROVENANCE_BUNDLE_FILE}`, provenance.bundleBytes],
|
|
147
|
+
];
|
|
148
|
+
for (const image of request.images) {
|
|
149
|
+
const collected = await collectImage({ image, request, context, clientFor, services });
|
|
150
|
+
images.push(collected.pin);
|
|
151
|
+
files.push(...collected.files);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const source = {
|
|
155
|
+
name: request.name,
|
|
156
|
+
repository: request.repository,
|
|
157
|
+
repositoryUrl,
|
|
158
|
+
repositoryId: request.repositoryId,
|
|
159
|
+
repositoryOwnerId: request.repositoryOwnerId,
|
|
160
|
+
sourceCommit: request.sourceCommit,
|
|
161
|
+
producerWorkflow: { path: workflowPath, sha: request.sourceCommit },
|
|
162
|
+
bundle: {
|
|
163
|
+
ociRef: request.bundleOciRef,
|
|
164
|
+
biomeId: request.biomeId,
|
|
165
|
+
sourceManifestHash,
|
|
166
|
+
signerPolicyId: row.policyId,
|
|
167
|
+
statementDigest: sha256Digest(provenance.statementBytes),
|
|
168
|
+
bundleDigest: sha256Digest(provenance.bundleBytes),
|
|
169
|
+
},
|
|
170
|
+
images: images.sort((left, right) => compare(left.imageRef, right.imageRef)),
|
|
171
|
+
};
|
|
172
|
+
const committed = loadThirdPartySourcePins(dataRoot).sources.filter((candidate) => candidate.name !== request.name);
|
|
173
|
+
const sources = [...committed, source].sort((left, right) => compare(left.name, right.name));
|
|
174
|
+
|
|
175
|
+
// (e) uniqueness, over the pins as they would be committed.
|
|
176
|
+
assertThirdPartyUniqueness({ sources, policy, runtimeSourceIndex: context.runtimeSourceIndex, biomeIndex: context.biomeIndex, platformServiceIndex: context.platformServiceIndex });
|
|
177
|
+
|
|
178
|
+
// (a) offline over the exact bytes to commit, (b) to (d): the very verification `--check-third-party` repeats.
|
|
179
|
+
const candidateIndex = schemas.parseRuntimeImageSourceIndex(
|
|
180
|
+
withProducerWorkflowPin({ committedIndex: context.runtimeSourceIndex, pin: { repository: request.repository, workflowCommit: request.sourceCommit } }),
|
|
181
|
+
);
|
|
182
|
+
const staging = mkdtempSync(join(tmpdir(), 'xema-third-party-write-'));
|
|
183
|
+
try {
|
|
184
|
+
for (const [path, bytes] of files) {
|
|
185
|
+
const target = join(staging, path);
|
|
186
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
187
|
+
writeFileSync(target, bytes, { flag: 'wx' });
|
|
188
|
+
}
|
|
189
|
+
verifyThirdPartySourceEvidence({ source, evidenceRoot: staging, context: { ...context, runtimeSourceIndex: candidateIndex } });
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(staging, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
return { source, sources, files, candidateIndex };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function writeNewOrIdentical(dataRoot, path, bytes) {
|
|
197
|
+
const target = join(dataRoot, path);
|
|
198
|
+
try {
|
|
199
|
+
writeFileSync(target, bytes, { flag: 'wx', mode: 0o644 });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error.code !== 'EEXIST') {throw error;}
|
|
202
|
+
if (!readFileSync(target).equals(bytes)) {
|
|
203
|
+
throw new Error(`${path} already exists with different bytes; evidence is content-addressed and is never overwritten`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Write a planned source: the evidence files (content-addressed, never overwritten), then the pin file, then the
|
|
210
|
+
* index. The evidence trust pin is NOT repinned: reviewing the pin file and running `xema edition repin-evidence-trust`
|
|
211
|
+
* is the intended next step, and until then the pin load refuses.
|
|
212
|
+
*
|
|
213
|
+
* @returns {string} the next step to print
|
|
214
|
+
*/
|
|
215
|
+
export function commitThirdPartySourceWrite({ dataRoot, plan, indexPath, indexText }) {
|
|
216
|
+
for (const [path, bytes] of plan.files) {
|
|
217
|
+
mkdirSync(dirname(join(dataRoot, path)), { recursive: true });
|
|
218
|
+
writeNewOrIdentical(dataRoot, path, bytes);
|
|
219
|
+
}
|
|
220
|
+
writeFileSync(
|
|
221
|
+
join(dataRoot, THIRD_PARTY_SOURCE_PINS_FILE),
|
|
222
|
+
canonicalJsonBytes({ schemaVersion: THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION, sources: plan.sources }),
|
|
223
|
+
);
|
|
224
|
+
writeFileSync(indexPath, indexText);
|
|
225
|
+
return (
|
|
226
|
+
`pinned third-party source "${plan.source.name}" (${plan.source.repository}@${plan.source.sourceCommit}, ${plan.source.images.length} image(s)); ` +
|
|
227
|
+
`run \`${REPIN_COMMAND}\` after reviewing ${THIRD_PARTY_SOURCE_PINS_FILE}: the evidence trust pin does not yet record it, so the pin load refuses until then`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Third-party OCI biomes on the resolve side (XEMA-1611 design section 4).
|
|
2
|
+
//
|
|
3
|
+
// A biome from an organisation outside the vendor fleet reaches an edition as a lock row with NO `source` (the lock's
|
|
4
|
+
// `LockedBiomeSource` is git-only), an OCI `bundleSource` and `origin: third-party`. That triple is the whole key; the
|
|
5
|
+
// pin file (`third-party-source-pins.json`, written only by the trusted writer) is what the row is then held to. The
|
|
6
|
+
// image subjects of such a row take `imageRef`, `sourceCommit` and `contentHash` from the pin and are never built,
|
|
7
|
+
// hashed or probed by this plane: there is no checkout to hash.
|
|
8
|
+
//
|
|
9
|
+
// `contentHash` of such a subject is the PUBLISHER'S statement (`sourceTreeHash` of its image-source evidence), never
|
|
10
|
+
// a hash this plane computed. It is therefore labelled `contentHash (publisher-attested)` wherever it is printed and
|
|
11
|
+
// is not used as an image tag, a cache key or a reproducibility check by any path that serves third-party subjects.
|
|
12
|
+
|
|
13
|
+
import { loadKernelBiomeContracts } from './lib.mjs';
|
|
14
|
+
|
|
15
|
+
export const PUBLISHER_ATTESTED_LABEL = 'publisher-attested';
|
|
16
|
+
|
|
17
|
+
export const OCI_BUNDLE_SOURCE_KIND = 'oci';
|
|
18
|
+
|
|
19
|
+
const hasOciBundleSource = (row) => row.bundleSource?.kind === OCI_BUNDLE_SOURCE_KIND;
|
|
20
|
+
const isThirdPartyOrigin = (row) => row.origin === loadKernelBiomeContracts().BiomeOrigin.ThirdParty;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The lock key of a third-party OCI biome: no `source`, an OCI `bundleSource`, `origin: third-party`.
|
|
24
|
+
* A lock row and a walked index entry both carry these fields; an index entry's `source` is the walk's stamp, so it
|
|
25
|
+
* is dropped by the caller that tests an entry (`isThirdPartyOciEntry`).
|
|
26
|
+
*/
|
|
27
|
+
export const isThirdPartyOciRow = (row) => row.source === undefined && hasOciBundleSource(row) && isThirdPartyOrigin(row);
|
|
28
|
+
|
|
29
|
+
/** A walked index entry that is a third-party OCI biome: its `source` is the walk's stamp, not a lock source. */
|
|
30
|
+
export const isThirdPartyOciEntry = (entry) => hasOciBundleSource(entry) && isThirdPartyOrigin(entry);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Refuse an OCI `bundleSource` on a row whose origin is not third-party: only a third-party biome is pulled from an OCI
|
|
34
|
+
* bundle by digest, so the vendor's origin on such a row would place an unevidenced bundle in the vendor's trust domain.
|
|
35
|
+
* A row drawn from a git `source` is not this key and carries no OCI `bundleSource` at all: resolve omits the manifest's
|
|
36
|
+
* (it names a release artifact not built from the pinned commit), and a lock stating one is refused (`locked-bundle-signer.mjs`).
|
|
37
|
+
*/
|
|
38
|
+
export function assertOciBundleSourceOrigin(row, label) {
|
|
39
|
+
if (row.source === undefined && hasOciBundleSource(row) && !isThirdPartyOrigin(row)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${label} carries an OCI bundleSource (${row.bundleSource.ociRef}) but its origin is "${row.origin}": an OCI ` +
|
|
42
|
+
`biome bundle is admitted only as origin "${loadKernelBiomeContracts().BiomeOrigin.ThirdParty}", with a pin of the data root's third-party source pins`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The third-party source pin a third-party OCI row is held to, by the row's `repo` (the pinned repository). Refuses a
|
|
49
|
+
* row with no pin (naming why the data root has none when it has none), a pin for another biome, another bundle
|
|
50
|
+
* reference or another manifest hash, and a pin the distribution does not declare as an `oci` source of that name.
|
|
51
|
+
*
|
|
52
|
+
* @param {{row: object, declaredSources: readonly object[]|undefined, pins: {sources: readonly object[], reason: string|null, byRepository: (repository: string) => object|undefined}|undefined, label: string}} options
|
|
53
|
+
*/
|
|
54
|
+
export function thirdPartyPinForRow({ row, pins, declaredSources, label }) {
|
|
55
|
+
if (pins === undefined) {
|
|
56
|
+
throw new Error(`${label} is a third-party OCI biome, but this resolve was given no third-party source pins to hold it to`);
|
|
57
|
+
}
|
|
58
|
+
const pin = pins.byRepository(row.repo);
|
|
59
|
+
if (pin === undefined) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${label} is a third-party OCI biome with neither a source nor a pin: ${pins.reason ?? `no third-party source pin names repository "${row.repo}"`}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (row.id !== pin.bundle.biomeId) {
|
|
65
|
+
throw new Error(`${label} has biome id "${row.id}", but the third-party pin "${pin.name}" is for biome "${pin.bundle.biomeId}"`);
|
|
66
|
+
}
|
|
67
|
+
if (row.bundleSource.ociRef !== pin.bundle.ociRef) {
|
|
68
|
+
throw new Error(`${label} names bundle ${row.bundleSource.ociRef}, but the third-party pin "${pin.name}" pins ${pin.bundle.ociRef}`);
|
|
69
|
+
}
|
|
70
|
+
if (row.sourceManifestHash !== pin.bundle.sourceManifestHash) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${label} has sourceManifestHash ${row.sourceManifestHash}, but the third-party pin "${pin.name}" pins ${pin.bundle.sourceManifestHash}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(declaredSources) || !declaredSources.some((source) => source.kind === OCI_BUNDLE_SOURCE_KIND && source.name === pin.name)) {
|
|
76
|
+
throw new Error(`${label} is drawn from the third-party pin "${pin.name}", which the distribution does not declare as a sources[] entry of kind "${OCI_BUNDLE_SOURCE_KIND}"`);
|
|
77
|
+
}
|
|
78
|
+
return pin;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The image subjects of one third-party OCI row, from its pin and nothing else: one per service and runtime
|
|
83
|
+
* component the row ships (a worker that runs its sibling's image has none). Every pinned image must be one of them and
|
|
84
|
+
* every shipped image must be pinned, so the pin and the lock describe the same images.
|
|
85
|
+
*
|
|
86
|
+
* `contentHash` is the publisher's `sourceTreeHash` (publisher-attested); `sourceCommit` is the pin's.
|
|
87
|
+
*/
|
|
88
|
+
export function thirdPartySubjectCandidates({ row, pin, enums, requiredPlatforms, label }) {
|
|
89
|
+
if (row.target === 'web') {
|
|
90
|
+
throw new Error(`${label} is a third-party OCI biome with target "web": a bundled web biome of a third party is not admitted (it has no pinned image)`);
|
|
91
|
+
}
|
|
92
|
+
const sharedImageWorkers = new Set(Object.keys(row.workerImages ?? {}));
|
|
93
|
+
const wanted = [
|
|
94
|
+
...(row.services ?? []).filter((service) => !sharedImageWorkers.has(service)).map((id) => [enums.RuntimeImageSubjectKind.Service, id]),
|
|
95
|
+
...(row.runtimeComponents ?? []).map((id) => [enums.RuntimeImageSubjectKind.RuntimeComponent, id]),
|
|
96
|
+
];
|
|
97
|
+
const byKey = new Map(pin.images.map((image) => [`${image.subjectKind}\0${image.subjectId}`, image]));
|
|
98
|
+
const used = new Set();
|
|
99
|
+
const candidates = wanted.map(([kind, id]) => {
|
|
100
|
+
const image = byKey.get(`${kind}\0${id}`);
|
|
101
|
+
if (image === undefined) {
|
|
102
|
+
throw new Error(`${label} ships ${kind} "${id}", which the third-party pin "${pin.name}" pins no image for`);
|
|
103
|
+
}
|
|
104
|
+
used.add(`${kind}\0${id}`);
|
|
105
|
+
return {
|
|
106
|
+
kind,
|
|
107
|
+
id,
|
|
108
|
+
imageRef: image.imageRef,
|
|
109
|
+
repo: row.repo,
|
|
110
|
+
path: row.path,
|
|
111
|
+
sourceCommit: pin.sourceCommit,
|
|
112
|
+
sourceManifestHash: row.sourceManifestHash,
|
|
113
|
+
requiredPlatforms,
|
|
114
|
+
owners: [{ kind: enums.RuntimeImageOwnerKind.Biome, biomeId: row.id }],
|
|
115
|
+
contentHash: image.sourceTreeHash,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
const unused = [...byKey.keys()].filter((key) => !used.has(key));
|
|
119
|
+
if (unused.length > 0) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`the third-party pin "${pin.name}" pins image(s) ${unused.map((key) => key.replace('\0', ':')).join(', ')} that ${label} does not ship`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return candidates;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A third-party subject's content hash, as every CLI output prints it. */
|
|
128
|
+
export const attestedContentHash = (subject) => `contentHash (${PUBLISHER_ATTESTED_LABEL}) ${subject.contentHash}`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Who may name which image repository in the evidence trust policy (XEMA-1611 design section 3(e)).
|
|
2
|
+
//
|
|
3
|
+
// Checked when the declared policy is LOADED, so every consumer of the policy sees it:
|
|
4
|
+
// (a) an `imageRepositories` entry listed by rows whose `githubWorkflow.repository` OWNERS differ is
|
|
5
|
+
// refused: one repository, one publisher;
|
|
6
|
+
// (b) a row whose owner is not a first-party owner and that names a first-party namespace
|
|
7
|
+
// (`<registry>/<first-party owner>/<any>`) is refused.
|
|
8
|
+
// First-party owners are the owners of the runtime-image-source index `repositoryPins` URLs, plus the kernel scope owner.
|
|
9
|
+
|
|
10
|
+
import { KERNEL_PACKAGE_NAME } from './kernel-package.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The vendor's publishing owner: the npm scope of the kernel package (`@<scope>/contracts`), read from the package name
|
|
14
|
+
* the CLI already pins, not written here. It is a first-party owner on purpose, beside the owners of the index's
|
|
15
|
+
* `repositoryPins`: the vendor publishes its kernel-scoped images under that owner, which no repository pin names, so
|
|
16
|
+
* without it a vendor row for them would be refused by rule (b) as a third party.
|
|
17
|
+
*/
|
|
18
|
+
export const KERNEL_SCOPE_OWNER = KERNEL_PACKAGE_NAME.replace(/^@/u, '').split('/')[0];
|
|
19
|
+
|
|
20
|
+
/** The lowercase owners of the `repositoryPins[].repositoryUrl` entries of a parsed runtime image source index, plus the kernel scope owner. */
|
|
21
|
+
export function firstPartyOwnersFromIndex(indexDocument) {
|
|
22
|
+
const pins = indexDocument?.repositoryPins;
|
|
23
|
+
if (!Array.isArray(pins)) {
|
|
24
|
+
throw new Error('runtime image source index has no repositoryPins array to derive the first-party owners from');
|
|
25
|
+
}
|
|
26
|
+
const owners = new Set([KERNEL_SCOPE_OWNER]);
|
|
27
|
+
for (const pin of pins) {
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(pin?.repositoryUrl);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(`runtime image source index repositoryPins entry ${JSON.stringify(pin?.repo)} has no parsable repositoryUrl`);
|
|
33
|
+
}
|
|
34
|
+
const owner = url.pathname.split('/').filter(Boolean)[0];
|
|
35
|
+
if (owner === undefined) {
|
|
36
|
+
throw new Error(`runtime image source index repositoryUrl ${url.href} names no owner`);
|
|
37
|
+
}
|
|
38
|
+
owners.add(owner.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
return owners;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether an image repository sits in a first-party namespace: `<registry>/<first-party owner>/<any>`, an owner being
|
|
45
|
+
* one of `firstPartyOwners` (the data root's own, from `firstPartyOwnersFromIndex`). The registry is whatever the first
|
|
46
|
+
* path segment is; no registry or organisation is named here.
|
|
47
|
+
*/
|
|
48
|
+
export function isFirstPartyImageNamespace(imageRepository, firstPartyOwners) {
|
|
49
|
+
const segments = imageRepository.toLowerCase().split('/');
|
|
50
|
+
return segments.slice(1, -1).some((segment) => firstPartyOwners.has(segment));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rowOwner = (row) => row.githubWorkflow?.repository?.split('/')[0]?.toLowerCase();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Refuse a policy that breaks (a) or (b). `firstPartyOwners` is `null` when the data root has no runtime image source
|
|
57
|
+
* index (a customer data root): (b) cannot be judged there and is not applied; (a) always is.
|
|
58
|
+
*
|
|
59
|
+
* @param {{policy: {policies: object[]}, firstPartyOwners: Set<string>|null}} options
|
|
60
|
+
*/
|
|
61
|
+
export function assertTrustPolicyImageOwnership({ policy, firstPartyOwners }) {
|
|
62
|
+
const ownersByRepository = new Map();
|
|
63
|
+
for (const row of policy.policies) {
|
|
64
|
+
const owner = rowOwner(row);
|
|
65
|
+
if (owner === undefined || !Array.isArray(row.imageRepositories)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const imageRepository of row.imageRepositories) {
|
|
69
|
+
const seen = ownersByRepository.get(imageRepository);
|
|
70
|
+
if (seen !== undefined && seen.owner !== owner) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`evidence trust policy: image repository ${imageRepository} is listed by rows of two publishers ` +
|
|
73
|
+
`(${seen.policyId} owned by ${seen.owner}, ${row.policyId} owned by ${owner}); one image repository has one publisher`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
ownersByRepository.set(imageRepository, { owner, policyId: row.policyId });
|
|
77
|
+
if (firstPartyOwners !== null && !firstPartyOwners.has(owner) && isFirstPartyImageNamespace(imageRepository, firstPartyOwners)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`evidence trust policy: row ${row.policyId} (owner ${owner}, not a first-party owner) names image repository ` +
|
|
80
|
+
`${imageRepository} in a first-party namespace (<registry>/<first-party owner>/<any>)`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -24,6 +24,7 @@ import { EDITION_CONTRACT } from './edition-constants.mjs';
|
|
|
24
24
|
import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot } from './edition-workspace.mjs';
|
|
25
25
|
import {
|
|
26
26
|
assertPinnedPolicy,
|
|
27
|
+
assertPinnedThirdPartySourcePins,
|
|
27
28
|
assertPinnedVerifier,
|
|
28
29
|
loadDeclaredTrustPolicy,
|
|
29
30
|
readEvidenceTrustPin,
|
|
@@ -143,6 +144,7 @@ async function armedAnchors({ channel, env, bundlePath }) {
|
|
|
143
144
|
const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot: dataRoot(), declaration, schemas });
|
|
144
145
|
const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
|
|
145
146
|
assertPinnedPolicy({ pin, policyDigest });
|
|
147
|
+
assertPinnedThirdPartySourcePins({ pin, dataRoot: dataRoot() });
|
|
146
148
|
assertPinnedVerifier({ pin, actual: verifierDigests(running) });
|
|
147
149
|
const { state, unmet } = assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue: true });
|
|
148
150
|
if (state !== TrustPolicyArmingState.ARMED) {
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
RUNTIME_IMAGE_SOURCE_INDEX_FILE,
|
|
46
46
|
assertPinnedKernelContractAuthority,
|
|
47
47
|
assertPinnedPolicy,
|
|
48
|
+
assertPinnedThirdPartySourcePins,
|
|
48
49
|
assertPinnedVerifier,
|
|
49
50
|
readStatedKernelContractAuthority,
|
|
50
51
|
loadDeclaredTrustPolicy,
|
|
@@ -264,6 +265,7 @@ function readTrustAnchors({ schemas, running }) {
|
|
|
264
265
|
});
|
|
265
266
|
const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
|
|
266
267
|
assertPinnedPolicy({ pin, policyDigest });
|
|
268
|
+
assertPinnedThirdPartySourcePins({ pin, dataRoot: dataRoot() });
|
|
267
269
|
assertPinnedVerifier({ pin, actual: verifierDigests(running) });
|
|
268
270
|
// The authority the producer statements are held to is read from the data
|
|
269
271
|
// root's index downstream; it is the pinned one, or nothing is verified.
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
splitImageRef,
|
|
56
56
|
} from './registry-credentials.mjs';
|
|
57
57
|
import { assertReleaseContent, assertReleaseEditionTree } from './release-channel-guard.mjs';
|
|
58
|
+
import { imageSubjectKey } from './third-party-image-pins.mjs';
|
|
58
59
|
|
|
59
60
|
if (isEntryModule(import.meta.url)) {
|
|
60
61
|
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Kernel]);
|
|
@@ -182,9 +183,15 @@ async function readManifestConfigPlatform(client, repository, manifest, label) {
|
|
|
182
183
|
* Resolve the set of platforms a published coordinate ACTUALLY serves.
|
|
183
184
|
* Every returned platform is proven by a hash-verified image config, never by
|
|
184
185
|
* an index descriptor's self-declaration alone.
|
|
186
|
+
*
|
|
187
|
+
* The coordinate is read by exactly one of `tag` (a first-party `<contentHash>` tag) or `digest` (a third-party
|
|
188
|
+
* subject, read by its pinned manifest digest).
|
|
185
189
|
*/
|
|
186
|
-
export async function resolvePublishedPlatforms({ client, repository, tag, coordinate }) {
|
|
187
|
-
|
|
190
|
+
export async function resolvePublishedPlatforms({ client, repository, tag, digest, coordinate }) {
|
|
191
|
+
if ((tag === undefined) === (digest === undefined)) {
|
|
192
|
+
throw new Error(`${coordinate} must be read by exactly one of a tag or a digest (got ${tag === undefined ? 'neither' : 'both'})`);
|
|
193
|
+
}
|
|
194
|
+
const top = tag === undefined ? await client.getManifestByDigest(repository, digest) : await client.getManifestByTag(repository, tag);
|
|
188
195
|
const platforms = [];
|
|
189
196
|
|
|
190
197
|
if (!top.isIndex) {
|
|
@@ -340,6 +347,9 @@ export async function verifyDistributionPlatforms({
|
|
|
340
347
|
collectFailures = false,
|
|
341
348
|
// `(line) => void`: one line per verified subject, with its elapsed ms; null is silent.
|
|
342
349
|
progress = null,
|
|
350
|
+
// `kind:id` -> manifest digest of the subjects pinned by a third-party source pin: each is read BY that digest, never by
|
|
351
|
+
// its (publisher-attested) contentHash as a tag.
|
|
352
|
+
pinnedDigests = new Map(),
|
|
343
353
|
}) {
|
|
344
354
|
const declarations = loadRegistryCredentialDeclarations(distributionsRoot);
|
|
345
355
|
|
|
@@ -369,9 +379,28 @@ export async function verifyDistributionPlatforms({
|
|
|
369
379
|
);
|
|
370
380
|
const subjects = verified.filter((entry) => entry !== undefined);
|
|
371
381
|
|
|
382
|
+
// A pinned image absent at its digest is a broken pin, never "not published yet" (no producer lane is about to build
|
|
383
|
+
// it): the failure is rethrown without the registry's `status`, so no caller's not-published-yet skip can take it.
|
|
384
|
+
async function readPublished({ client, repository, subject, pinnedDigest, coordinate }) {
|
|
385
|
+
if (pinnedDigest === undefined) {
|
|
386
|
+
return resolvePublishedPlatforms({ client, repository, tag: subject.contentHash, coordinate });
|
|
387
|
+
}
|
|
388
|
+
let published;
|
|
389
|
+
try {
|
|
390
|
+
published = await resolvePublishedPlatforms({ client, repository, digest: pinnedDigest, coordinate });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
throw new Error(`pinned third-party image ${coordinate} (${subject.kind}:${subject.id}) cannot be read: ${error.message}`, { cause: error });
|
|
393
|
+
}
|
|
394
|
+
if (published.indexDigest !== pinnedDigest) {
|
|
395
|
+
throw new Error(`${coordinate} served manifest ${published.indexDigest} for ${subject.kind}:${subject.id}, not the pinned ${pinnedDigest}`);
|
|
396
|
+
}
|
|
397
|
+
return published;
|
|
398
|
+
}
|
|
399
|
+
|
|
372
400
|
async function verifySubject(subject) {
|
|
373
401
|
const { authority, repository } = splitImageRef(subject.imageRef);
|
|
374
|
-
const
|
|
402
|
+
const pinnedDigest = pinnedDigests.get(imageSubjectKey(subject));
|
|
403
|
+
const coordinate = pinnedDigest === undefined ? `${subject.imageRef}:${subject.contentHash}` : `${subject.imageRef}@${pinnedDigest}`;
|
|
375
404
|
// A subject that requires no platform cannot be verified — every check
|
|
376
405
|
// would pass vacuously and the image would ship unexamined.
|
|
377
406
|
if (
|
|
@@ -384,12 +413,7 @@ export async function verifyDistributionPlatforms({
|
|
|
384
413
|
);
|
|
385
414
|
}
|
|
386
415
|
const client = clients.get(authority);
|
|
387
|
-
const published = await
|
|
388
|
-
client,
|
|
389
|
-
repository,
|
|
390
|
-
tag: subject.contentHash,
|
|
391
|
-
coordinate,
|
|
392
|
-
});
|
|
416
|
+
const published = await readPublished({ client, repository, subject, pinnedDigest, coordinate });
|
|
393
417
|
const present = published.platforms.map((entry) =>
|
|
394
418
|
platformIdentity(entry.platform),
|
|
395
419
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/xema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/xema-dev/xema-cli.git",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"@xemahq/biome-sdk": "^22.0.0",
|
|
53
53
|
"@xemahq/biome-supply-chain": "^0.5.0",
|
|
54
54
|
"@xemahq/distribution-source-hash": "^0.2.2",
|
|
55
|
-
"@xemahq/dsl": "^0.
|
|
56
|
-
"@xemahq/identity-client": "^0.
|
|
57
|
-
"@xemahq/contracts": "^0.
|
|
55
|
+
"@xemahq/dsl": "^0.28.0",
|
|
56
|
+
"@xemahq/identity-client": "^0.36.0",
|
|
57
|
+
"@xemahq/contracts": "^0.112.0",
|
|
58
58
|
"@xemahq/kernel-state-runtime": "^0.8.0",
|
|
59
59
|
"@xemahq/license-verifier": "^0.2.0",
|
|
60
60
|
"@xemahq/service-registry-nest": "^0.12.2",
|
|
@@ -92,6 +92,6 @@
|
|
|
92
92
|
"typecheck": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"xemaRelease": {
|
|
95
|
-
"sourceDigest": "sha256:
|
|
95
|
+
"sourceDigest": "sha256:f330048c083619658f4721c67c1c4df25e1a9b64b2664a513e871b970d281639"
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { IdentityApiClient } from '@xemahq/identity-client';
|
|
2
|
-
export interface RegisterCliClientOptions {
|
|
3
|
-
readonly identityApiUrl: string;
|
|
4
|
-
readonly internalToken: string;
|
|
5
|
-
readonly realm: string;
|
|
6
|
-
readonly identityClient?: Pick<IdentityApiClient, 'registerServiceFull'>;
|
|
7
|
-
}
|
|
8
|
-
export declare const IDENTITY_API_INTERNAL_TOKEN = "IDENTITY_API_INTERNAL_TOKEN";
|
|
9
|
-
export declare function resolveIdentityRegistrationToken(configuredToken: string | undefined, environmentToken: string | undefined): string;
|
|
10
|
-
export declare function registerCliDeviceClient(options: RegisterCliClientOptions): Promise<void>;
|
|
11
|
-
//# sourceMappingURL=client-bootstrap.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/client-bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAIlB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,CAAC;CAC1E;AAaD,eAAO,MAAM,2BAA2B,gCAAgC,CAAC;AAQzE,wBAAgB,gCAAgC,CAC9C,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,MAAM,CAmBR;AAOD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAsBf"}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.IDENTITY_API_INTERNAL_TOKEN = void 0;
|
|
4
|
-
exports.resolveIdentityRegistrationToken = resolveIdentityRegistrationToken;
|
|
5
|
-
exports.registerCliDeviceClient = registerCliDeviceClient;
|
|
6
|
-
const identity_client_1 = require("@xemahq/identity-client");
|
|
7
|
-
const device_flow_1 = require("./device-flow");
|
|
8
|
-
const PUBLIC_DEVICE_SERVICE_TYPE = 'PUBLIC_DEVICE';
|
|
9
|
-
const CLI_REGISTRATION = Object.freeze({
|
|
10
|
-
name: device_flow_1.DEFAULT_CLI_CLIENT_ID,
|
|
11
|
-
displayName: 'Xema CLI',
|
|
12
|
-
description: 'Public device-authorization client for the Xema command-line interface',
|
|
13
|
-
serviceType: PUBLIC_DEVICE_SERVICE_TYPE,
|
|
14
|
-
regenerateSecret: false,
|
|
15
|
-
});
|
|
16
|
-
exports.IDENTITY_API_INTERNAL_TOKEN = 'IDENTITY_API_INTERNAL_TOKEN';
|
|
17
|
-
function resolveIdentityRegistrationToken(configuredToken, environmentToken) {
|
|
18
|
-
const configured = normalizeToken(configuredToken, 'configured secret');
|
|
19
|
-
const environment = normalizeToken(environmentToken, 'process environment');
|
|
20
|
-
if (configured !== undefined &&
|
|
21
|
-
environment !== undefined &&
|
|
22
|
-
configured !== environment) {
|
|
23
|
-
throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} differs between the configured secret and process environment. Refusing ambiguous identity authority.`);
|
|
24
|
-
}
|
|
25
|
-
const token = configured ?? environment;
|
|
26
|
-
if (token === undefined) {
|
|
27
|
-
throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} is required to reconcile the ${device_flow_1.DEFAULT_CLI_CLIENT_ID} public device client. Declare that exact secret or environment variable before launch.`);
|
|
28
|
-
}
|
|
29
|
-
return token;
|
|
30
|
-
}
|
|
31
|
-
async function registerCliDeviceClient(options) {
|
|
32
|
-
const client = options.identityClient ??
|
|
33
|
-
new identity_client_1.IdentityApiClient({
|
|
34
|
-
baseUrl: options.identityApiUrl,
|
|
35
|
-
internalToken: options.internalToken,
|
|
36
|
-
});
|
|
37
|
-
const registration = await client.registerServiceFull({
|
|
38
|
-
...CLI_REGISTRATION,
|
|
39
|
-
realm: options.realm,
|
|
40
|
-
});
|
|
41
|
-
if (registration.id !== device_flow_1.DEFAULT_CLI_CLIENT_ID) {
|
|
42
|
-
throw new Error(`identity-api registration response did not identify ${device_flow_1.DEFAULT_CLI_CLIENT_ID}.`);
|
|
43
|
-
}
|
|
44
|
-
if (registration.clientSecret !== null) {
|
|
45
|
-
throw new Error(`identity-api returned a client secret for public client ${device_flow_1.DEFAULT_CLI_CLIENT_ID}.`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function normalizeToken(token, source) {
|
|
49
|
-
if (token === undefined) {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
if (token.length === 0 || token.trim().length === 0) {
|
|
53
|
-
throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} from ${source} is empty.`);
|
|
54
|
-
}
|
|
55
|
-
if (token !== token.trim()) {
|
|
56
|
-
throw new Error(`${exports.IDENTITY_API_INTERNAL_TOKEN} from ${source} has leading or trailing whitespace.`);
|
|
57
|
-
}
|
|
58
|
-
return token;
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=client-bootstrap.js.map
|