@xemahq/xema 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +22 -2
- 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 +224 -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 +47 -6
- 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 +127 -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
|
@@ -1,11 +1,95 @@
|
|
|
1
1
|
// edition-contract-parse — the strict parse of edition-contract.json. A consumer
|
|
2
2
|
// that vendors the contract holds it to this same shape: a contract that is not
|
|
3
|
-
//
|
|
4
|
-
// refused rather than read with a default.
|
|
3
|
+
// v3, or lacks `repositories`, or names a channel, kind or evidence trust state
|
|
4
|
+
// this CLI does not know, is refused rather than read with a default.
|
|
5
5
|
|
|
6
6
|
import { EditionChannel } from './edition-channel.mjs';
|
|
7
|
+
import { EditionKind, EvidenceTrustState } from './edition-kind.mjs';
|
|
7
8
|
|
|
8
|
-
export const EDITION_CONTRACT_SCHEMA = 'xema.edition-contract/
|
|
9
|
+
export const EDITION_CONTRACT_SCHEMA = 'xema.edition-contract/v3';
|
|
10
|
+
|
|
11
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
const sameSet = (left, right) => Array.isArray(left) && left.length === right.length && right.every((name) => left.includes(name));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The descriptor vocabulary: the descriptor schema (read from the contract, never restated), the closed kind enum,
|
|
16
|
+
* `sealOf` (its pattern, its repository rule and the one channel a seal is of), the signers (the issuer, and the SHAPE of
|
|
17
|
+
* a publisher: which kinds and channels have one and its {repository, workflow, ref} grammar; the publisher VALUES are
|
|
18
|
+
* the data root's edition-workspace.json, never the contract's) and the evidence trust states each kind may record per
|
|
19
|
+
* channel. A kind's channels are the same in `signers.publishers.channelsByKind` and `evidenceTrust.statesByKind`: every
|
|
20
|
+
* channel for a resolved edition, exactly `sealOf.channel` for a sealed one (a seal is release-only).
|
|
21
|
+
*/
|
|
22
|
+
/** `source` is an anchored (^...$) regular expression source that compiles. */
|
|
23
|
+
function assertAnchoredPattern(source, where) {
|
|
24
|
+
if (typeof source !== 'string' || !source.startsWith('^') || !source.endsWith('$')) {
|
|
25
|
+
throw new Error(`edition-contract.json: ${where} must be an anchored (^...$) regular expression source`);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
new RegExp(source);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`edition-contract.json: ${where} is not a regular expression: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertDescriptorVocabulary(contract) {
|
|
35
|
+
if (typeof contract.descriptorSchema !== 'string' || contract.descriptorSchema === '') {
|
|
36
|
+
throw new Error('edition-contract.json: "descriptorSchema" must be a non-empty string');
|
|
37
|
+
}
|
|
38
|
+
const kinds = Object.values(EditionKind);
|
|
39
|
+
const channels = Object.values(EditionChannel);
|
|
40
|
+
if (!sameSet(contract.descriptorKinds, kinds)) {
|
|
41
|
+
throw new Error(`edition-contract.json: descriptorKinds must be exactly ${kinds.join(' | ')} (got ${JSON.stringify(contract.descriptorKinds)})`);
|
|
42
|
+
}
|
|
43
|
+
const sealOf = contract.sealOf;
|
|
44
|
+
const texts = ['names', 'repository', 'sealedEdition'];
|
|
45
|
+
if (!isObject(sealOf) || !sameSet(Object.keys(sealOf), ['pattern', 'channel', ...texts]) || !texts.every((key) => typeof sealOf[key] === 'string' && sealOf[key] !== '')) {
|
|
46
|
+
throw new Error('edition-contract.json: "sealOf" must be exactly {pattern, names, repository, channel, sealedEdition}: the one statement of a sealed edition\'s sealOf');
|
|
47
|
+
}
|
|
48
|
+
assertAnchoredPattern(sealOf.pattern, 'sealOf.pattern');
|
|
49
|
+
if (!channels.includes(sealOf.channel)) {
|
|
50
|
+
throw new Error(`edition-contract.json: sealOf.channel must be one of ${channels.join(' | ')}`);
|
|
51
|
+
}
|
|
52
|
+
const channelsOf = { [EditionKind.Resolved]: channels, [EditionKind.Sealed]: [sealOf.channel] };
|
|
53
|
+
const signers = contract.signers;
|
|
54
|
+
if (!isObject(signers) || !sameSet(Object.keys(signers), ['oidcIssuer', 'identity', 'publishers']) || !/^https:\/\/\S+$/.test(signers.oidcIssuer ?? '') || typeof signers.identity !== 'string' || signers.identity === '') {
|
|
55
|
+
throw new Error('edition-contract.json: "signers" must be exactly {oidcIssuer: <https URL>, identity, publishers}');
|
|
56
|
+
}
|
|
57
|
+
const publishers = signers.publishers;
|
|
58
|
+
if (!isObject(publishers) || !sameSet(Object.keys(publishers), ['values', 'channelsByKind', 'shape']) || typeof publishers.values !== 'string' || publishers.values === '') {
|
|
59
|
+
throw new Error('edition-contract.json: signers.publishers must be exactly {values, channelsByKind, shape}');
|
|
60
|
+
}
|
|
61
|
+
const fields = ['repository', 'workflow', 'ref'];
|
|
62
|
+
if (!isObject(publishers.shape) || !sameSet(Object.keys(publishers.shape), fields)) {
|
|
63
|
+
throw new Error(`edition-contract.json: signers.publishers.shape must be exactly {${fields.join(', ')}}`);
|
|
64
|
+
}
|
|
65
|
+
for (const field of fields) {
|
|
66
|
+
assertAnchoredPattern(publishers.shape[field], `signers.publishers.shape.${field}`);
|
|
67
|
+
}
|
|
68
|
+
const trust = contract.evidenceTrust;
|
|
69
|
+
const states = Object.values(EvidenceTrustState);
|
|
70
|
+
if (!isObject(trust) || !isObject(trust.states) || !sameSet(Object.keys(trust.states), states)) {
|
|
71
|
+
throw new Error(`edition-contract.json: evidenceTrust.states must name exactly ${states.join(' | ')}`);
|
|
72
|
+
}
|
|
73
|
+
for (const [where, byKind] of [['signers.publishers.channelsByKind', publishers.channelsByKind], ['evidenceTrust.statesByKind', trust.statesByKind]]) {
|
|
74
|
+
if (!isObject(byKind) || !sameSet(Object.keys(byKind), kinds)) {
|
|
75
|
+
throw new Error(`edition-contract.json: ${where} must have exactly one entry per kind (${kinds.join(' | ')})`);
|
|
76
|
+
}
|
|
77
|
+
for (const kind of kinds) {
|
|
78
|
+
const named = Array.isArray(byKind[kind]) ? byKind[kind] : isObject(byKind[kind]) ? Object.keys(byKind[kind]) : undefined;
|
|
79
|
+
if (!sameSet(named, channelsOf[kind]) || new Set(named).size !== named.length) {
|
|
80
|
+
throw new Error(`edition-contract.json: ${where}.${kind} must have exactly one entry per channel of a ${kind} edition (${channelsOf[kind].join(' | ')})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const kind of kinds) {
|
|
85
|
+
for (const channel of channelsOf[kind]) {
|
|
86
|
+
const admitted = trust.statesByKind[kind][channel];
|
|
87
|
+
if (!Array.isArray(admitted) || admitted.length === 0 || new Set(admitted).size !== admitted.length || !admitted.every((state) => states.includes(state))) {
|
|
88
|
+
throw new Error(`edition-contract.json: evidenceTrust.statesByKind.${kind}.${channel} must be a non-empty set of ${states.join(' | ')}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
9
93
|
|
|
10
94
|
const SEGMENT = /^[a-z0-9][a-z0-9.-]*$/;
|
|
11
95
|
|
|
@@ -48,5 +132,6 @@ export function parseEditionContract(contract) {
|
|
|
48
132
|
if (segments.size !== channels.length) {
|
|
49
133
|
throw new Error('edition-contract.json: the channels\' namespace segments must differ');
|
|
50
134
|
}
|
|
135
|
+
assertDescriptorVocabulary(contract);
|
|
51
136
|
return contract;
|
|
52
137
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schema": "xema.edition-contract/
|
|
2
|
+
"schema": "xema.edition-contract/v3",
|
|
3
3
|
"vendoring": "This file is the single source of the edition artifact coordinates and names. A consumer vendors it BYTE-IDENTICAL from a stated xema-cli commit and never edits it. It reads both repository namespace segments (repositories.<channel>.namespaceSegment) and the repository prefix (repositoryPrefix) only from this file, and refuses a contract that has no `repositories`.",
|
|
4
4
|
"artifactType": "application/vnd.xema.edition.v1+json",
|
|
5
5
|
"repositoryPrefix": "xema-edition-",
|
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"descriptorFile": "xema-edition.json",
|
|
15
|
-
"descriptorSchema": "xema.edition/
|
|
15
|
+
"descriptorSchema": "xema.edition/v2",
|
|
16
|
+
"descriptorKinds": [
|
|
17
|
+
"resolved",
|
|
18
|
+
"sealed"
|
|
19
|
+
],
|
|
20
|
+
"sealOf": {
|
|
21
|
+
"pattern": "^[a-z0-9.-]+(:[0-9]+)?(/[a-z0-9._-]+)+@sha256:[0-9a-f]{64}$",
|
|
22
|
+
"names": "the digest-pinned ref (<registry>/<path>@sha256:<64 hex>) of the one resolved edition a sealed edition seals; a sealed descriptor carries it and no other descriptor does. A consumer reads the pattern from here (a JavaScript RegExp source, anchored) and restates it nowhere",
|
|
23
|
+
"repository": "beyond the pattern: the repository of sealOf (everything before @) is an edition repository, <registry root>/<repositories.<channel>.namespaceSegment>/<repositoryPrefix><distributionId>, whose channel is sealOf.channel and whose distributionId is the sealed descriptor's distributionId",
|
|
24
|
+
"channel": "release",
|
|
25
|
+
"sealedEdition": "a seal is release-only: only a resolved edition of sealOf.channel is sealed (a resolved edition of any other channel is refused), and the sealed edition is pushed to the repository of that channel (repositories.<sealOf.channel>) and records provenance.channel sealOf.channel"
|
|
26
|
+
},
|
|
16
27
|
"locks": [
|
|
17
28
|
"distribution.lock.json",
|
|
18
29
|
"images.lock.json",
|
|
@@ -30,11 +41,43 @@
|
|
|
30
41
|
"evidenceTrust": {
|
|
31
42
|
"states": {
|
|
32
43
|
"armed": "the data root declares an evidence trust policy that is pinned and ARMED on the channel of the edition",
|
|
33
|
-
"not-armed": "a
|
|
44
|
+
"not-armed": "a RESOLVED edition of the develop channel only (vendor or customer): its data root declares no policy, or one that is UNARMED on the develop channel. A customer develop edition always records it while a develop catalogue carries no edition-catalogue (E4) evidence. A sealed edition, and a resolved edition of any other channel, never records not-armed"
|
|
45
|
+
},
|
|
46
|
+
"statesByKind": {
|
|
47
|
+
"resolved": {
|
|
48
|
+
"develop": [
|
|
49
|
+
"armed",
|
|
50
|
+
"not-armed"
|
|
51
|
+
],
|
|
52
|
+
"release": [
|
|
53
|
+
"armed"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"sealed": {
|
|
57
|
+
"release": [
|
|
58
|
+
"armed"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
34
61
|
},
|
|
35
62
|
"refusal": {
|
|
36
63
|
"code": "EVIDENCE_TRUST_REQUIRED_UNARMED",
|
|
37
|
-
"when": "
|
|
64
|
+
"when": "an edition is written while the data root's evidence trust policy is not armed on the channel of the edition (none declared, or declared but UNARMED or PARTIALLY ARMED there) and statesByKind admits only armed for its kind on that channel (every sealed edition; a resolved edition of any channel but develop): no descriptor or artifact is written, and no configuration key admits one. A resolved develop edition records not-armed instead, except under a policy PARTIALLY ARMED on develop, which is refused for every edition"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"signers": {
|
|
68
|
+
"oidcIssuer": "https://token.actions.githubusercontent.com",
|
|
69
|
+
"identity": "the exact keyless certificate identity of a publisher is https://github.com/<repository>/<workflow>@<ref>, issued by oidcIssuer and by nothing else; no wildcard, and no identity signs both kinds",
|
|
70
|
+
"publishers": {
|
|
71
|
+
"values": "the publisher of an edition of each kind on each channel is data root data, never this contract's: <data root>/edition-workspace.json editionPublishers.<kind>.<channel>, each exactly publishers.shape; a publisher the data root does not declare is refused by name, with no default and no fallback",
|
|
72
|
+
"channelsByKind": {
|
|
73
|
+
"resolved": ["develop", "release"],
|
|
74
|
+
"sealed": ["release"]
|
|
75
|
+
},
|
|
76
|
+
"shape": {
|
|
77
|
+
"repository": "^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9._-]+$",
|
|
78
|
+
"workflow": "^\\.github/workflows/[A-Za-z0-9._-]+\\.ya?ml$",
|
|
79
|
+
"ref": "^refs/heads/[A-Za-z0-9._/-]+$"
|
|
80
|
+
}
|
|
38
81
|
}
|
|
39
82
|
}
|
|
40
83
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// sealed is the digest the registry serves for `<repository>:<contentHash>`.
|
|
11
11
|
//
|
|
12
12
|
// A coordinate that is not there is "not built yet": the producer's CI is not
|
|
13
|
-
// green or still running.
|
|
13
|
+
// green or still running. A third-party subject (a pinned OCI biome's image) is never re-homed and never "not built":
|
|
14
|
+
// it keeps its pinned imageRef and is verified BY the pin's manifest digest (third-party-image-pins.mjs). EVERY such subject is collected and named at once;
|
|
14
15
|
// any other failure (a 403, a malformed index, an absent required platform)
|
|
15
16
|
// aborts as it does in the release.
|
|
16
17
|
|
|
@@ -20,6 +21,7 @@ import { EditionChannel } from './edition-channel.mjs';
|
|
|
20
21
|
import { editionsRoot } from './edition-workspace.mjs';
|
|
21
22
|
import { validateImmutableImageLock, loadKernelDistributionSchemas, } from './lib.mjs';
|
|
22
23
|
import { isNotPublishedYet, planDigestSeal } from './seal-image-digests.mjs';
|
|
24
|
+
import { imageSubjectKey, pinnedManifestDigests, thirdPartyImagePins } from './third-party-image-pins.mjs';
|
|
23
25
|
import { verifyDistributionPlatforms } from './verify-image-platforms.mjs';
|
|
24
26
|
|
|
25
27
|
/** The repository a channel's own image subjects are verified and sealed in, from the subject's release imageRef. */
|
|
@@ -45,21 +47,25 @@ export function channelImageRef(channel, releaseImageRef) {
|
|
|
45
47
|
* @param {string} request.channel `develop` or `release`: which repositories the subjects are sealed in
|
|
46
48
|
* @param {string} request.distributionId the edition the subjects belong to (named in refusals)
|
|
47
49
|
* @param {object[]} request.subjects image subjects (release refs, unsealed)
|
|
50
|
+
* @param {{byRepository: (repository: string) => object|undefined}} [request.thirdPartyPins] the data root's third-party
|
|
51
|
+
* source pins: a subject of a pinned repository keeps its imageRef and is sealed at the pin's manifest digest
|
|
48
52
|
* @returns the subjects with the channel's refs and verified digests
|
|
49
53
|
*/
|
|
50
54
|
export async function sealImageSubjects({
|
|
51
55
|
channel,
|
|
52
56
|
distributionId,
|
|
53
57
|
subjects,
|
|
58
|
+
thirdPartyPins = undefined,
|
|
54
59
|
distributionsRoot = editionsRoot(),
|
|
55
60
|
env = process.env,
|
|
56
61
|
fetchImpl = globalThis.fetch,
|
|
57
62
|
run = null,
|
|
58
63
|
progress = null,
|
|
59
64
|
}) {
|
|
65
|
+
const pinnedImages = thirdPartyPins === undefined ? new Map() : thirdPartyImagePins({ pins: thirdPartyPins, subjects });
|
|
60
66
|
const channelSubjects = subjects.map((subject) => ({
|
|
61
67
|
...subject,
|
|
62
|
-
imageRef: channelImageRef(channel, subject.imageRef),
|
|
68
|
+
imageRef: pinnedImages.has(imageSubjectKey(subject)) ? subject.imageRef : channelImageRef(channel, subject.imageRef),
|
|
63
69
|
}));
|
|
64
70
|
const report = await verifyDistributionPlatforms({
|
|
65
71
|
imageLock: { distributionId, subjects: channelSubjects },
|
|
@@ -69,6 +75,7 @@ export async function sealImageSubjects({
|
|
|
69
75
|
run,
|
|
70
76
|
collectFailures: true,
|
|
71
77
|
progress,
|
|
78
|
+
pinnedDigests: pinnedManifestDigests(pinnedImages),
|
|
72
79
|
});
|
|
73
80
|
const aborting = report.failures.find(({ error }) => !isNotPublishedYet(error));
|
|
74
81
|
if (aborting) {throw aborting.error;}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// edition-kind — the closed vocabularies of an edition descriptor (xema.edition/v2) that the
|
|
2
|
+
// contract (edition-contract.json) names: the lock KIND and the evidence trust STATE. They are
|
|
3
|
+
// enums here so a reader branches on names, and edition-contract-parse holds the contract to
|
|
4
|
+
// exactly these sets: a contract that names another kind or state is refused, never read.
|
|
5
|
+
|
|
6
|
+
/** What an edition IS: a lock resolved by a resolve run, or the seal of one exact resolved edition (`sealOf`). */
|
|
7
|
+
export const EditionKind = Object.freeze({ Resolved: 'resolved', Sealed: 'sealed' });
|
|
8
|
+
|
|
9
|
+
/** Whether the data root's editions are evidence-gated, as the descriptor records it. */
|
|
10
|
+
export const EvidenceTrustState = Object.freeze({ Armed: 'armed', NotArmed: 'not-armed' });
|
|
@@ -31,11 +31,17 @@ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
|
31
31
|
/** The edition protocols this library interprets: version -> content digest. A protocol is listed only
|
|
32
32
|
* while every kernel published under it exports what this library imports (`kernel-export-coverage.test.mjs`):
|
|
33
33
|
* v27 and older kernels lack `serviceImageSubjects` (first exported by the v28 kernel), so they are not listed.
|
|
34
|
-
* No published kernel declares v29 (0.110.2 is v28, 0.110.3 is v30, 0.111.0 is v31
|
|
34
|
+
* No published kernel declares v29 (0.110.2 is v28, 0.110.3 is v30, 0.111.0 is v31, XEMA-2070 kernel is v32; v33 is the kernel's
|
|
35
|
+
* bundle-signer protocol (`bundleSigner` on every OCI bundle row); v34 is the kernel's output-mapping, JSON Pointer and
|
|
36
|
+
* handoff-repair-turns closure additions (XEMA-2122); v33 and v34 are not yet published: their identities and contracts are captured from the
|
|
37
|
+
* linked kernel), so v29 is not listed either. */
|
|
35
38
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
36
39
|
Object.freeze({ version: 28, contentDigest: 'f7795478b1bfe9a41bcbea9efe496011fcd209a56c90294029a08801e84cf92c' }),
|
|
37
40
|
Object.freeze({ version: 30, contentDigest: '91e3cdff255778b5aa94b64ca9316d031be549ead2267a9a9311ff06d4f1db2c' }),
|
|
38
41
|
Object.freeze({ version: 31, contentDigest: 'b2026847fdd202e7b9e05815cdc2c9819b60be7007c789cc5b5f291c9c682fdd' }),
|
|
42
|
+
Object.freeze({ version: 32, contentDigest: 'a4ca824486052ca7f61b9a68f759b798defb620b6b3e9cbaab2c701bee2c6700' }),
|
|
43
|
+
Object.freeze({ version: 33, contentDigest: 'b070713e7abeeefad6da3a9053843b0f7420a466f6d504f60aaa00ef196b2b05' }),
|
|
44
|
+
Object.freeze({ version: 34, contentDigest: '370ceef5956d66035265ddbf7b9c46aa686cd10c1e95171c806c0d590ef5f49c' }),
|
|
39
45
|
]);
|
|
40
46
|
|
|
41
47
|
/** Where a kernel package publishes its identity and its contract, package-relative. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// edition-publisher — the grammar of an edition publisher: the exact GitHub Actions workflow whose keyless
|
|
2
|
+
// signature admits an edition. `{repository, workflow, ref}`, each held to edition-contract.json
|
|
3
|
+
// signers.publishers.shape; its certificate identity is `https://github.com/<repository>/<workflow>@<ref>`
|
|
4
|
+
// (signers.identity), never a wildcard. The issuer is the contract's (edition-constants.mjs); the publisher
|
|
5
|
+
// VALUES are the data root's (edition-workspace.json editionPublishers, edition-config.mjs).
|
|
6
|
+
|
|
7
|
+
import { EDITION_CONTRACT } from './edition-constants.mjs';
|
|
8
|
+
|
|
9
|
+
const SHAPE = Object.freeze(Object.fromEntries(Object.entries(EDITION_CONTRACT.signers.publishers.shape).map(([field, source]) => [field, new RegExp(source)])));
|
|
10
|
+
|
|
11
|
+
/** Hold `publisher` to exactly `{repository, workflow, ref}`, each in the contract's shape; returns it. */
|
|
12
|
+
export function assertPublisher(publisher, where) {
|
|
13
|
+
const keys = publisher !== null && typeof publisher === 'object' && !Array.isArray(publisher) ? Object.keys(publisher).sort().join() : '';
|
|
14
|
+
if (keys !== 'ref,repository,workflow' || !Object.entries(SHAPE).every(([field, pattern]) => typeof publisher[field] === 'string' && pattern.test(publisher[field]))) {
|
|
15
|
+
throw new Error(`${where} must be exactly {repository: <owner>/<name>, workflow: .github/workflows/<file>.yaml, ref: refs/heads/<branch>} (edition-contract.json signers.publishers.shape; got ${JSON.stringify(publisher)})`);
|
|
16
|
+
}
|
|
17
|
+
return publisher;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The exact keyless certificate identity of a publisher. */
|
|
21
|
+
export const publisherIdentity = ({ repository, workflow, ref }) => `https://github.com/${repository}/${workflow}@${ref}`;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// edition-seal — seal ONE exact resolved edition.
|
|
3
|
+
//
|
|
4
|
+
// xema edition seal --resolved <registry root>/<channel segment>/xema-edition-<id>@sha256:<64 hex>
|
|
5
|
+
// --manifest <file> --artifact <dir> --out <dir>
|
|
6
|
+
// --cosign <absolute path> --resolve-workflow-sha <40 hex>
|
|
7
|
+
// [--local]
|
|
8
|
+
// --data-root <dir> --editions-root <dir> --kernel <dir>
|
|
9
|
+
//
|
|
10
|
+
// FIRST, the seal verifies its input and fails closed: `cosign verify` of --resolved (the manifest digest),
|
|
11
|
+
// through the one pinned-cosign path (pinned-cosign.mjs: the caller's cosign held to the digest the data root's
|
|
12
|
+
// pinned policy names, `--offline` against the policy's pinned trusted root), with every certificate constraint
|
|
13
|
+
// (cosign-certificate-constraints.mjs): the identity of the RESOLVE publisher, the contract's issuer (never
|
|
14
|
+
// overridden), its workflow repository and ref, and --resolve-workflow-sha, the commit the resolve workflow ran
|
|
15
|
+
// at (it only narrows the constraint: a wrong value fails the verify). The publisher is the data root's resolved
|
|
16
|
+
// publisher of the --resolved repository's channel (edition-workspace.json editionPublishers.resolved.<channel>,
|
|
17
|
+
// refused by name when undeclared: no default, and no flag names another). Its repository must be the
|
|
18
|
+
// repository the resolved descriptor records as its workflow (provenance.workflow.repository), checked before
|
|
19
|
+
// cosign runs. A refused or unrunnable verify writes nothing.
|
|
20
|
+
//
|
|
21
|
+
// The caller pulls the resolved edition BY DIGEST (`oras manifest fetch` into --manifest, `oras pull`
|
|
22
|
+
// into --artifact). A seal is RELEASE-ONLY (edition-contract.json sealOf.channel, D99): a --resolved edition of any
|
|
23
|
+
// other channel is refused by name, and the sealed edition records provenance.channel release and goes to the
|
|
24
|
+
// release repository. Nothing pulled is trusted until it is proven to be the edition --resolved names:
|
|
25
|
+
//
|
|
26
|
+
// * the manifest bytes hash to the --resolved digest, it is an edition artifact (artifactType), and
|
|
27
|
+
// every layer is titled with a file an edition may hold, once;
|
|
28
|
+
// * every pulled file hashes to the digest of its layer, and the artifact holds no other file;
|
|
29
|
+
// * the descriptor parses (xema.edition/v2) and is `kind: resolved`, of the distribution and the
|
|
30
|
+
// channel the --resolved repository names.
|
|
31
|
+
//
|
|
32
|
+
// The sealed edition (written to --out, outside the committed trees) holds every layer of the resolved
|
|
33
|
+
// edition but its descriptor BYTE FOR BYTE, plus a `kind: sealed` descriptor naming --resolved as `sealOf`,
|
|
34
|
+
// with the resolved edition's channel (release), pins and engine image and the seal run as provenance.workflow.
|
|
35
|
+
// A seal must be ARMED (edition-contract.json evidenceTrust.statesByKind): the data root's evidence trust
|
|
36
|
+
// policy is judged on the resolved edition's channel, and anything but armed is refused by name
|
|
37
|
+
// (EvidenceTrustRequiredError, EVIDENCE_TRUST_REQUIRED_UNARMED). The sealed repository is printed:
|
|
38
|
+
// the same registry root, the release namespace segment. The workflow pushes --out there and signs the
|
|
39
|
+
// manifest digest with the SEAL identity; who may seal is the signer's, not this command's.
|
|
40
|
+
|
|
41
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
42
|
+
import { join, resolve } from 'node:path';
|
|
43
|
+
|
|
44
|
+
import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs';
|
|
45
|
+
import { LOCAL_WORKFLOW, editionRepository, parseEditionDescriptor, parseSealOf, workflowFromEnv, writeSealedEditionArtifact } from './edition-artifact.mjs';
|
|
46
|
+
import { editionPublisher } from './edition-config.mjs';
|
|
47
|
+
import {
|
|
48
|
+
CATALOGUE_PIN_KEY,
|
|
49
|
+
EDITION_ARTIFACT_TYPE,
|
|
50
|
+
EDITION_DESCRIPTOR_FILE,
|
|
51
|
+
EDITION_FILES,
|
|
52
|
+
EDITION_SIGNER_OIDC_ISSUER,
|
|
53
|
+
EDITION_OFFERING_LAYERS,
|
|
54
|
+
EditionKind,
|
|
55
|
+
SEALED_EDITION_CHANNEL,
|
|
56
|
+
} from './edition-constants.mjs';
|
|
57
|
+
import { publisherIdentity } from './edition-publisher.mjs';
|
|
58
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
|
|
59
|
+
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
60
|
+
import { sha256Hex } from './lib.mjs';
|
|
61
|
+
import { cosignEnvironment, withPinnedCosign } from './pinned-cosign.mjs';
|
|
62
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
63
|
+
import { parseStrictJson } from './strict-json.mjs';
|
|
64
|
+
|
|
65
|
+
if (isEntryModule(import.meta.url)) {
|
|
66
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Kernel]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const REQUIRED_FLAGS = Object.freeze(['--resolved', '--manifest', '--artifact', '--out', '--cosign', '--resolve-workflow-sha']);
|
|
70
|
+
const VALUE_FLAGS = REQUIRED_FLAGS;
|
|
71
|
+
const USAGE =
|
|
72
|
+
'usage: seal --resolved <repository>@sha256:<digest> --manifest <file> --artifact <dir> --out <dir> --cosign <absolute path> ' +
|
|
73
|
+
'--resolve-workflow-sha <40 hex> [--local]';
|
|
74
|
+
const WORKFLOW_SHA = /^[0-9a-f]{40}$/;
|
|
75
|
+
const TITLE = 'org.opencontainers.image.title';
|
|
76
|
+
/** Every file an edition artifact may hold: the descriptor, the locks, the offering layers. */
|
|
77
|
+
const ADMITTED_FILES = Object.freeze([...EDITION_FILES, ...Object.values(EDITION_OFFERING_LAYERS)]);
|
|
78
|
+
|
|
79
|
+
/** Parse the argv after the `seal` verb: every value flag exactly once, `--local` optional, nothing else. */
|
|
80
|
+
export function parseSealArgs(argv) {
|
|
81
|
+
const options = {};
|
|
82
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
83
|
+
const arg = argv[i];
|
|
84
|
+
if (arg === '--local') {
|
|
85
|
+
options[arg] = true;
|
|
86
|
+
} else if (VALUE_FLAGS.includes(arg)) {
|
|
87
|
+
const value = argv[i + 1];
|
|
88
|
+
if (value === undefined || value.startsWith('--')) {throw new Error(`${arg} requires a value`);}
|
|
89
|
+
if (arg in options) {throw new Error(`${arg} is given twice`);}
|
|
90
|
+
options[arg] = value;
|
|
91
|
+
i += 1;
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error(`unknown argument ${arg} for seal (${USAGE})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const missing = REQUIRED_FLAGS.filter((flag) => !(flag in options));
|
|
97
|
+
if (missing.length > 0) {throw new Error(`seal requires ${missing.join(', ')} (${USAGE})`);}
|
|
98
|
+
if (!WORKFLOW_SHA.test(options['--resolve-workflow-sha'])) {
|
|
99
|
+
throw new Error(`--resolve-workflow-sha ${JSON.stringify(options['--resolve-workflow-sha'])} is not the 40-hex commit the resolve workflow ran at`);
|
|
100
|
+
}
|
|
101
|
+
return options;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Prove the pulled manifest and files are the resolved edition `sealOf` names, and return
|
|
106
|
+
* `{ resolved, layers }`: its parsed descriptor and every other layer's bytes by title.
|
|
107
|
+
*
|
|
108
|
+
* @param {{ sealOf: string, manifestBytes: Buffer, artifactDir: string }} input
|
|
109
|
+
*/
|
|
110
|
+
export function readResolvedEdition({ sealOf, manifestBytes, artifactDir }) {
|
|
111
|
+
const ref = parseSealOf(sealOf);
|
|
112
|
+
if (ref.channel !== SEALED_EDITION_CHANNEL) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`${sealOf} is a ${ref.channel} edition: a seal is ${SEALED_EDITION_CHANNEL}-only (edition-contract.json sealOf.channel); ` +
|
|
115
|
+
`seal the resolved ${SEALED_EDITION_CHANNEL} edition of "${ref.distributionId}" instead`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (`sha256:${sha256Hex(manifestBytes)}` !== ref.digest) {
|
|
119
|
+
throw new Error(`the manifest given hashes to sha256:${sha256Hex(manifestBytes)}, not to ${ref.digest}: it is not the manifest of ${sealOf}`);
|
|
120
|
+
}
|
|
121
|
+
const manifest = parseStrictJson(manifestBytes.toString('utf8'), 'the manifest');
|
|
122
|
+
if (manifest?.artifactType !== EDITION_ARTIFACT_TYPE || !Array.isArray(manifest.layers)) {
|
|
123
|
+
throw new Error(`${sealOf} is not an edition artifact (artifactType ${JSON.stringify(manifest?.artifactType)}, expected ${EDITION_ARTIFACT_TYPE})`);
|
|
124
|
+
}
|
|
125
|
+
const digestByTitle = new Map();
|
|
126
|
+
for (const layer of manifest.layers) {
|
|
127
|
+
const title = layer?.annotations?.[TITLE];
|
|
128
|
+
if (!ADMITTED_FILES.includes(title)) {
|
|
129
|
+
throw new Error(`${sealOf} carries a layer titled ${JSON.stringify(title)}, which is not a file an edition holds (${ADMITTED_FILES.join(', ')})`);
|
|
130
|
+
}
|
|
131
|
+
if (digestByTitle.has(title)) {throw new Error(`${sealOf} carries two layers titled ${title}`);}
|
|
132
|
+
digestByTitle.set(title, layer.digest);
|
|
133
|
+
}
|
|
134
|
+
const pulled = readdirSync(artifactDir).sort();
|
|
135
|
+
const expected = [...digestByTitle.keys()].sort();
|
|
136
|
+
if (pulled.join('\0') !== expected.join('\0')) {
|
|
137
|
+
throw new Error(`${artifactDir} holds ${pulled.join(', ') || 'nothing'}, but ${sealOf} has the layers ${expected.join(', ')}; pull exactly that artifact`);
|
|
138
|
+
}
|
|
139
|
+
const layers = new Map();
|
|
140
|
+
let descriptorBytes;
|
|
141
|
+
for (const [title, digest] of digestByTitle) {
|
|
142
|
+
const bytes = readFileSync(join(artifactDir, title));
|
|
143
|
+
if (`sha256:${sha256Hex(bytes)}` !== digest) {
|
|
144
|
+
throw new Error(`${join(artifactDir, title)} hashes to sha256:${sha256Hex(bytes)}, not to its layer digest ${digest} in ${sealOf}`);
|
|
145
|
+
}
|
|
146
|
+
if (title === EDITION_DESCRIPTOR_FILE) {descriptorBytes = bytes;} else {layers.set(title, bytes);}
|
|
147
|
+
}
|
|
148
|
+
if (descriptorBytes === undefined) {throw new Error(`${sealOf} carries no ${EDITION_DESCRIPTOR_FILE}`);}
|
|
149
|
+
const resolved = parseEditionDescriptor(parseStrictJson(descriptorBytes.toString('utf8'), `${sealOf} ${EDITION_DESCRIPTOR_FILE}`));
|
|
150
|
+
if (resolved.kind !== EditionKind.Resolved) {
|
|
151
|
+
throw new Error(`${sealOf} is a ${resolved.kind} edition; a seal seals exactly one resolved edition`);
|
|
152
|
+
}
|
|
153
|
+
if (resolved.distributionId !== ref.distributionId || resolved.provenance.channel !== ref.channel) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`${sealOf} is the ${ref.channel} repository of "${ref.distributionId}", but its descriptor records the ${resolved.provenance.channel} edition "${resolved.distributionId}"`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return { resolved, layers };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The resolve publisher a seal verifies against: ONLY the data root's edition-workspace.json
|
|
163
|
+
* editionPublishers.resolved.<channel> (refused by name when undeclared; no flag or default names another), and only
|
|
164
|
+
* when its repository is the one the resolved descriptor records as its workflow: a seal never verifies an edition
|
|
165
|
+
* against a publisher its own provenance does not name. Checked before cosign runs.
|
|
166
|
+
*/
|
|
167
|
+
export function resolvePublisher(resolved, sealOf) {
|
|
168
|
+
const channel = resolved.provenance.channel;
|
|
169
|
+
const publisher = editionPublisher(EditionKind.Resolved, channel);
|
|
170
|
+
const recorded = resolved.provenance.workflow?.repository;
|
|
171
|
+
if (publisher.repository !== recorded) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`${sealOf} records its resolve workflow in ${JSON.stringify(recorded)}, but the data root's resolved ${channel} publisher ` +
|
|
174
|
+
`(edition-workspace.json editionPublishers.resolved.${channel}) is in "${publisher.repository}": a seal verifies an edition only ` +
|
|
175
|
+
'against the publisher its provenance names; nothing is sealed',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return publisher;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `cosign verify` of `sealOf` against the exact resolve publisher, through the one pinned-cosign path. Throws, naming
|
|
183
|
+
* the publisher, when cosign refuses (unsigned, another identity, another commit) or cannot run; returns nothing.
|
|
184
|
+
*
|
|
185
|
+
* @param {{ sealOf: string, publisher: {repository: string, workflow: string, ref: string}, workflowSha: string, cosignBinary: string, env: object, pinned?: object }} input
|
|
186
|
+
* `pinned` are withPinnedCosign's injections (tests)
|
|
187
|
+
*/
|
|
188
|
+
export async function verifyResolvedSignature({ sealOf, publisher, workflowSha, cosignBinary, env, pinned = {} }) {
|
|
189
|
+
const signer = {
|
|
190
|
+
certificateIdentity: publisherIdentity(publisher),
|
|
191
|
+
oidcIssuer: EDITION_SIGNER_OIDC_ISSUER,
|
|
192
|
+
githubWorkflow: { repository: publisher.repository, ref: publisher.ref },
|
|
193
|
+
};
|
|
194
|
+
const constraints = certificateConstraintArgs(signer, workflowSha);
|
|
195
|
+
const { authority } = splitImageRef(parseSealOf(sealOf).repository);
|
|
196
|
+
await withPinnedCosign({ cosignBinary, env, ...pinned }, async ({ runCosign, cosignEnvFor, trustedRootPath }) => {
|
|
197
|
+
let result;
|
|
198
|
+
try {
|
|
199
|
+
result = await runCosign(['verify', '--offline', '--trusted-root', trustedRootPath, ...constraints, sealOf], cosignEnvironment(cosignEnvFor, authority));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new Error(`cosign could not run for ${sealOf}: ${error.message}; nothing is sealed`);
|
|
202
|
+
}
|
|
203
|
+
if (result.code !== 0) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`cosign refused ${sealOf} as signed by the resolve publisher ${signer.certificateIdentity} (issuer ${EDITION_SIGNER_OIDC_ISSUER}, ` +
|
|
206
|
+
`workflow commit ${workflowSha}) (exit ${result.code}): ${String(result.stderr).trim()}; nothing is sealed`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Seal: prove the pulled edition by digest, verify the resolve publisher's signature over it, require an ARMED policy
|
|
214
|
+
* on its channel, write the sealed edition. Returns `{ descriptor, repository }`; the repository is where the sealed edition goes.
|
|
215
|
+
*/
|
|
216
|
+
export async function sealEdition({ options, env, evidenceTrust = descriptorEvidenceTrust, pinned = {} }) {
|
|
217
|
+
const sealOf = options['--resolved'];
|
|
218
|
+
const { resolved, layers } = readResolvedEdition({
|
|
219
|
+
sealOf,
|
|
220
|
+
manifestBytes: readFileSync(options['--manifest']),
|
|
221
|
+
artifactDir: resolve(options['--artifact']),
|
|
222
|
+
});
|
|
223
|
+
const channel = resolved.provenance.channel;
|
|
224
|
+
await verifyResolvedSignature({
|
|
225
|
+
sealOf,
|
|
226
|
+
publisher: resolvePublisher(resolved, sealOf),
|
|
227
|
+
workflowSha: options['--resolve-workflow-sha'],
|
|
228
|
+
cosignBinary: options['--cosign'],
|
|
229
|
+
env,
|
|
230
|
+
pinned,
|
|
231
|
+
});
|
|
232
|
+
const evidenceTrustPolicy = await evidenceTrust({
|
|
233
|
+
kind: EditionKind.Sealed,
|
|
234
|
+
channel,
|
|
235
|
+
consumesCatalogue: CATALOGUE_PIN_KEY in resolved.provenance.pins,
|
|
236
|
+
});
|
|
237
|
+
const descriptor = writeSealedEditionArtifact({
|
|
238
|
+
outDir: resolve(options['--out']),
|
|
239
|
+
resolved,
|
|
240
|
+
sealOf,
|
|
241
|
+
layers,
|
|
242
|
+
workflow: options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env),
|
|
243
|
+
evidenceTrustPolicy,
|
|
244
|
+
});
|
|
245
|
+
const repository = editionRepository(resolved.distributionId, parseSealOf(sealOf).registryRoot, SEALED_EDITION_CHANNEL);
|
|
246
|
+
return { descriptor, repository };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isEntryModule(import.meta.url)) {
|
|
250
|
+
Promise.resolve()
|
|
251
|
+
.then(() => sealEdition({ options: parseSealArgs(process.argv.slice(2)), env: process.env }))
|
|
252
|
+
.then(({ repository }) => console.log(repository))
|
|
253
|
+
.catch((error) => {
|
|
254
|
+
console.error(`[edition-seal] ${error.message}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -12,5 +12,7 @@ export const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
12
12
|
export const SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
13
13
|
/** `owner/repository` of a GitHub repository. */
|
|
14
14
|
export const GITHUB_REPOSITORY_PATTERN = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
15
|
+
/** A policy, subject or run identifier: alphanumeric ends, `._:/@+-` inside, at most 256 characters. */
|
|
16
|
+
export const CANONICAL_IDENTIFIER = /^[a-zA-Z0-9](?:[a-zA-Z0-9._:/@+-]{0,254}[a-zA-Z0-9])?$/;
|
|
15
17
|
/** A GitHub branch or tag ref. */
|
|
16
18
|
export const GITHUB_REF_PATTERN = /^refs\/(?:heads|tags)\/.+$/;
|