@xemahq/xema 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -37
- package/dist/assets/appliance-base/images.lock.json +126 -126
- package/dist/assets/appliance-lean/images.lock.json +123 -123
- package/dist/assets/cloud/images.lock.json +126 -126
- package/dist/assets/core/images.lock.json +120 -120
- package/dist/assets/distribution-assets.manifest.json +15 -16
- package/dist/assets/minimal/images.lock.json +108 -108
- package/dist/assets/oss/images.lock.json +126 -126
- package/dist/assets/starter/images.lock.json +123 -123
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +6 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/backing-registration.d.ts.map +1 -1
- package/dist/runtime/backing-registration.js +7 -2
- package/dist/runtime/backing-registration.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +3 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +20 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +3 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +37 -8
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +3 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +2 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +18 -7
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -154
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// The edition library's roots: every directory the tooling reads from is stated
|
|
2
|
+
// by the entry, once, through `configureEditionWorkspace`. Nothing is derived
|
|
3
|
+
// from where this file sits, and no root has a default: a getter on a root the
|
|
4
|
+
// entry did not state throws, naming the flag that states it.
|
|
5
|
+
|
|
6
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
export const WorkspaceRoot = Object.freeze({
|
|
11
|
+
Data: 'dataRoot',
|
|
12
|
+
Editions: 'editionsRoot',
|
|
13
|
+
Sources: 'sourcesRoot',
|
|
14
|
+
Kernel: 'kernelPackageDir',
|
|
15
|
+
Deploy: 'deployRoot',
|
|
16
|
+
Cache: 'cacheDir',
|
|
17
|
+
SourceHash: 'sourceHashPackageDir',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const WORKSPACE_FLAGS = Object.freeze({
|
|
21
|
+
[WorkspaceRoot.Data]: '--data-root',
|
|
22
|
+
[WorkspaceRoot.Editions]: '--editions-root',
|
|
23
|
+
[WorkspaceRoot.Sources]: '--sources-root',
|
|
24
|
+
[WorkspaceRoot.Kernel]: '--kernel',
|
|
25
|
+
[WorkspaceRoot.Deploy]: '--deploy-root',
|
|
26
|
+
[WorkspaceRoot.Cache]: '--cache-dir',
|
|
27
|
+
[WorkspaceRoot.SourceHash]: '--source-hash',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const ROOT_MEANING = Object.freeze({
|
|
31
|
+
[WorkspaceRoot.Data]:
|
|
32
|
+
'the directory holding the catalogue indices and the trust policy',
|
|
33
|
+
[WorkspaceRoot.Editions]:
|
|
34
|
+
'the directory holding <edition-id>/ definitions and locks',
|
|
35
|
+
[WorkspaceRoot.Sources]:
|
|
36
|
+
'the directory holding source checkouts at repos/<r> and submodules/<r>',
|
|
37
|
+
[WorkspaceRoot.Kernel]: 'the BUILT kernel-contracts package directory',
|
|
38
|
+
[WorkspaceRoot.Deploy]: 'the deploy (chart tree) checkout',
|
|
39
|
+
[WorkspaceRoot.Cache]: 'a writable directory for fetched artifacts',
|
|
40
|
+
[WorkspaceRoot.SourceHash]:
|
|
41
|
+
'the BUILT distribution-source-hash package directory (the source-tree and image-input hash implementation)',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const KERNEL_PACKAGE_SUBPATH = ['packages', 'kernel', 'kernel-contracts'];
|
|
45
|
+
|
|
46
|
+
let configured = Object.freeze({});
|
|
47
|
+
|
|
48
|
+
function assertDirectory(name, value) {
|
|
49
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
50
|
+
throw new Error(`${name} must be a non-empty directory path`);
|
|
51
|
+
}
|
|
52
|
+
const path = resolve(value);
|
|
53
|
+
if (!existsSync(path) || !statSync(path).isDirectory()) {
|
|
54
|
+
throw new Error(`${name} ${path} is not an existing directory`);
|
|
55
|
+
}
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* State the roots. Only the roots a caller passes are configured; the rest stay
|
|
61
|
+
* unset and throw when read. Replaces any previous configuration.
|
|
62
|
+
*/
|
|
63
|
+
export function configureEditionWorkspace(roots) {
|
|
64
|
+
const next = {};
|
|
65
|
+
for (const name of Object.values(WorkspaceRoot)) {
|
|
66
|
+
if (roots[name] !== undefined) {next[name] = assertDirectory(name, roots[name]);}
|
|
67
|
+
}
|
|
68
|
+
for (const name of Object.keys(roots)) {
|
|
69
|
+
if (!Object.values(WorkspaceRoot).includes(name)) {
|
|
70
|
+
throw new Error(`unknown workspace root "${name}"`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
configured = Object.freeze(next);
|
|
74
|
+
return configured;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function root(name) {
|
|
78
|
+
const value = configured[name];
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`the edition workspace has no ${name} (${ROOT_MEANING[name]}); state it with ${WORKSPACE_FLAGS[name]} <dir>`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const dataRoot = () => root(WorkspaceRoot.Data);
|
|
88
|
+
export const editionsRoot = () => root(WorkspaceRoot.Editions);
|
|
89
|
+
export const sourcesRoot = () => root(WorkspaceRoot.Sources);
|
|
90
|
+
export const kernelPackageDir = () => root(WorkspaceRoot.Kernel);
|
|
91
|
+
export const deployRoot = () => root(WorkspaceRoot.Deploy);
|
|
92
|
+
export const cacheDir = () => root(WorkspaceRoot.Cache);
|
|
93
|
+
export const sourceHashPackageDir = () => root(WorkspaceRoot.SourceHash);
|
|
94
|
+
|
|
95
|
+
/** A source checkout at its aggregator-relative path, e.g. `repos/xema-base`. */
|
|
96
|
+
export const sourcePath = (repo) => join(sourcesRoot(), repo);
|
|
97
|
+
|
|
98
|
+
/** The kernel-contracts checkout the built package directory sits in. */
|
|
99
|
+
export function kernelRepoDir() {
|
|
100
|
+
return resolve(kernelPackageDir(), ...KERNEL_PACKAGE_SUBPATH.map(() => '..'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A built subpath of the kernel package, e.g. `dist/distribution/index.js`. */
|
|
104
|
+
export const kernelDistPath = (...segments) =>
|
|
105
|
+
join(kernelPackageDir(), 'dist', ...segments);
|
|
106
|
+
|
|
107
|
+
export const biomeIndexPath = () => join(dataRoot(), 'biome-index.json');
|
|
108
|
+
export const platformServiceIndexPath = () =>
|
|
109
|
+
join(dataRoot(), 'platform-service-index.json');
|
|
110
|
+
export const runtimeImageSourceIndexPath = () =>
|
|
111
|
+
join(dataRoot(), 'runtime-image-source-index.json');
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse the workspace flags out of an argv (both `--flag v` and `--flag=v`),
|
|
115
|
+
* configure the roots, and return the remaining argv. `needs` names the roots
|
|
116
|
+
* the entry requires: a missing one is refused here, before any work.
|
|
117
|
+
*/
|
|
118
|
+
export function configureEditionWorkspaceFromArgv(argv, needs) {
|
|
119
|
+
const byFlag = new Map(
|
|
120
|
+
Object.entries(WORKSPACE_FLAGS).map(([name, flag]) => [flag, name]),
|
|
121
|
+
);
|
|
122
|
+
const roots = {};
|
|
123
|
+
const rest = [];
|
|
124
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
125
|
+
const arg = argv[i];
|
|
126
|
+
const eq = arg?.indexOf('=') ?? -1;
|
|
127
|
+
const flag = eq > 0 ? arg.slice(0, eq) : arg;
|
|
128
|
+
const name = byFlag.get(flag);
|
|
129
|
+
if (name === undefined) {
|
|
130
|
+
rest.push(arg);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const value = eq > 0 ? arg.slice(eq + 1) : argv[(i += 1)];
|
|
134
|
+
if (typeof value !== 'string' || value.length === 0 || value.startsWith('--')) {
|
|
135
|
+
throw new Error(`${flag} requires a directory path`);
|
|
136
|
+
}
|
|
137
|
+
roots[name] = value;
|
|
138
|
+
}
|
|
139
|
+
for (const name of needs) {
|
|
140
|
+
if (roots[name] === undefined) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`${WORKSPACE_FLAGS[name]} <dir> is required (${ROOT_MEANING[name]})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
configureEditionWorkspace(roots);
|
|
147
|
+
return rest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The roots configured so far, as the object `configureEditionWorkspace` takes. */
|
|
151
|
+
export const configuredWorkspace = () => ({ ...configured });
|
|
152
|
+
|
|
153
|
+
/** The flags that re-state the configured roots, for a child entry process. */
|
|
154
|
+
export function workspaceArgv() {
|
|
155
|
+
return Object.entries(configured).flatMap(([name, value]) => [
|
|
156
|
+
WORKSPACE_FLAGS[name],
|
|
157
|
+
value,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** True when `moduleUrl` is the script node was started with. */
|
|
162
|
+
export function isEntryModule(moduleUrl) {
|
|
163
|
+
return (
|
|
164
|
+
typeof process.argv[1] === 'string' &&
|
|
165
|
+
pathToFileURL(realpathSync(process.argv[1])).href === moduleUrl
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The entry's one call: configure the roots from `process.argv` and remove the
|
|
171
|
+
* root flags from it, so the entry's own parser sees only its own arguments.
|
|
172
|
+
*/
|
|
173
|
+
export function configureEditionWorkspaceFromProcess(needs) {
|
|
174
|
+
const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), needs);
|
|
175
|
+
process.argv.splice(2, process.argv.length - 2, ...rest);
|
|
176
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Grades the checkouts the evidence verifier reads from: the data root and the
|
|
2
|
+
// editions root. Each is judged to ONE standard — a trusted git binary, a
|
|
3
|
+
// location no other user can write, an exact HEAD commit, every file the
|
|
4
|
+
// verifier reads bound byte-for-byte to its HEAD blob, and no untrusted change
|
|
5
|
+
// anywhere in the repository (the commit is what the completeness signature
|
|
6
|
+
// binds to, so a checkout that is not exactly that commit is not that commit).
|
|
7
|
+
//
|
|
8
|
+
// Nothing here knows where this module sits: the verifier's own integrity is
|
|
9
|
+
// bound by the data-root pin of its bundle (`evidence-trust-policy.mjs`), not by
|
|
10
|
+
// a checkout of its source.
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
14
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { COMMIT_PATTERN } from './evidence-grammars.mjs';
|
|
17
|
+
import {
|
|
18
|
+
assertPreparedExecutable,
|
|
19
|
+
exactExecutableOutput,
|
|
20
|
+
prepareRootOwnedPinnedExecutable,
|
|
21
|
+
} from './trusted-executable.mjs';
|
|
22
|
+
|
|
23
|
+
const GIT_VERSION_PATTERN = /^git version (\d+\.\d+\.\d+)(?: .*)?\n?$/;
|
|
24
|
+
const GIT_ENV = Object.freeze({
|
|
25
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
26
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
27
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
28
|
+
GIT_NO_REPLACE_OBJECTS: '1',
|
|
29
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
30
|
+
GIT_PAGER: 'cat',
|
|
31
|
+
LC_ALL: 'C',
|
|
32
|
+
PATH: '/usr/bin:/bin',
|
|
33
|
+
});
|
|
34
|
+
const GIT_SAFE_CONFIG = Object.freeze([
|
|
35
|
+
'-c',
|
|
36
|
+
'core.fsmonitor=false',
|
|
37
|
+
'-c',
|
|
38
|
+
'core.hooksPath=/dev/null',
|
|
39
|
+
'-c',
|
|
40
|
+
'core.attributesFile=/dev/null',
|
|
41
|
+
'--no-pager',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The largest output one git call here may return. Node's default `maxBuffer`
|
|
46
|
+
* is 1 MiB, and a graded file can be larger (the Sigstore trusted root is up to
|
|
47
|
+
* 8 MiB); with the default the verifier could not bind it to its HEAD blob and
|
|
48
|
+
* would fail with `spawnSync git ENOBUFS`, which names neither the file nor the
|
|
49
|
+
* limit. 64 MiB is a bound, not a hope: far above any file graded here and far
|
|
50
|
+
* below anything that would matter to the host.
|
|
51
|
+
*/
|
|
52
|
+
const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
53
|
+
|
|
54
|
+
function git(trustedGit, directory, args, encoding = 'utf8') {
|
|
55
|
+
const gitBinary = assertPreparedExecutable(trustedGit, 'git');
|
|
56
|
+
const output = execFileSync(
|
|
57
|
+
gitBinary,
|
|
58
|
+
[...GIT_SAFE_CONFIG, '-C', directory, ...args],
|
|
59
|
+
{
|
|
60
|
+
encoding,
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
env: GIT_ENV,
|
|
63
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
assertPreparedExecutable(trustedGit, 'git');
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function prepareTrustedGitBinary({ gitBinary, policy }) {
|
|
71
|
+
return prepareRootOwnedPinnedExecutable({
|
|
72
|
+
executablePath: gitBinary,
|
|
73
|
+
trustedDigests: policy.trustedGitBinaryDigests,
|
|
74
|
+
label: 'git',
|
|
75
|
+
verifyVersion(privateBinary) {
|
|
76
|
+
const output = exactExecutableOutput(privateBinary, ['--version'], {
|
|
77
|
+
env: GIT_ENV,
|
|
78
|
+
timeout: 10_000,
|
|
79
|
+
});
|
|
80
|
+
const match = GIT_VERSION_PATTERN.exec(output);
|
|
81
|
+
if (!match) {
|
|
82
|
+
throw new Error('Git version output is not exact');
|
|
83
|
+
}
|
|
84
|
+
if (match[1] !== policy.gitVersion) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Git version does not match pinned ${policy.gitVersion}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertContained(root, candidate, label, within) {
|
|
94
|
+
const rel = relative(root, candidate);
|
|
95
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
96
|
+
throw new Error(`${label} is outside ${within}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `path` and every ancestor up to `/` must be owned by root or the current user
|
|
102
|
+
* and not group/world writable. The refusal names the offending directory, its
|
|
103
|
+
* mode and its owner: the fault is almost always one specific ancestor, and the
|
|
104
|
+
* reader cannot otherwise tell which, nor whether it failed on ownership or on
|
|
105
|
+
* the write bits.
|
|
106
|
+
*/
|
|
107
|
+
export function assertProtectedPathAncestry(path, label) {
|
|
108
|
+
const currentUid =
|
|
109
|
+
typeof process.getuid === 'function' ? process.getuid() : undefined;
|
|
110
|
+
let current = realpathSync(path);
|
|
111
|
+
while (true) {
|
|
112
|
+
const stats = lstatSync(current);
|
|
113
|
+
const foreignOwner =
|
|
114
|
+
currentUid !== undefined && stats.uid !== 0 && stats.uid !== currentUid;
|
|
115
|
+
const groupOrWorldWritable = (stats.mode & 0o022) !== 0;
|
|
116
|
+
if (foreignOwner || groupOrWorldWritable) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`${label} and every ancestor must be verifier/root-owned and not group/world writable — ` +
|
|
119
|
+
`${current} is ${foreignOwner ? `owned by uid ${stats.uid} (running as uid ${currentUid})` : `mode ${(stats.mode & 0o7777).toString(8).padStart(4, '0')}`}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (current === '/') {break;}
|
|
123
|
+
current = dirname(current);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dirtyPaths(trustedGit, repositoryRoot) {
|
|
128
|
+
const records = git(
|
|
129
|
+
trustedGit,
|
|
130
|
+
repositoryRoot,
|
|
131
|
+
['status', '--porcelain=v1', '-z', '--untracked-files=all'],
|
|
132
|
+
'buffer',
|
|
133
|
+
)
|
|
134
|
+
.toString('utf8')
|
|
135
|
+
.split('\0')
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
const entries = [];
|
|
138
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
139
|
+
const record = records[index];
|
|
140
|
+
const status = record.slice(0, 2);
|
|
141
|
+
entries.push({ status, path: record.slice(3) });
|
|
142
|
+
if (status[0] === 'R' || status[0] === 'C') {
|
|
143
|
+
entries.push({ status, path: records[index + 1] });
|
|
144
|
+
index += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return entries;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertSafeWorktreeFile(path, expectedMode, label) {
|
|
151
|
+
const stats = lstatSync(path);
|
|
152
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
153
|
+
throw new Error(`${label} must be a no-follow regular file`);
|
|
154
|
+
}
|
|
155
|
+
if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) {
|
|
156
|
+
throw new Error(`${label} is not owned by the current user`);
|
|
157
|
+
}
|
|
158
|
+
const actualMode = stats.mode & 0o777;
|
|
159
|
+
if (actualMode !== expectedMode || (actualMode & 0o022) !== 0) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`${label} must have exact mode ${expectedMode.toString(8)}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseTreeRecord(record, expectedPath, label) {
|
|
167
|
+
const match = /^(100644|100755) blob ([a-f0-9]{40,64})\t(.+)$/.exec(record);
|
|
168
|
+
if (!match || match[3] !== expectedPath) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${label} "${expectedPath}" is absent or has an untrusted HEAD mode`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return { mode: match[1], objectId: match[2] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assertNormalIndexFlags(trustedGit, repositoryRoot, path, label) {
|
|
177
|
+
const tagged = git(
|
|
178
|
+
trustedGit,
|
|
179
|
+
repositoryRoot,
|
|
180
|
+
['ls-files', '--stage', '-v', '-z', '--', path],
|
|
181
|
+
'buffer',
|
|
182
|
+
)
|
|
183
|
+
.toString('utf8')
|
|
184
|
+
.split('\0')
|
|
185
|
+
.filter(Boolean);
|
|
186
|
+
if (tagged.length !== 1 || !tagged[0].startsWith('H ')) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`${label} "${path}" has skip-worktree, assume-unchanged, or unexpected index state`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const debug = git(trustedGit, repositoryRoot, [
|
|
192
|
+
'ls-files',
|
|
193
|
+
'--debug',
|
|
194
|
+
'--',
|
|
195
|
+
path,
|
|
196
|
+
]);
|
|
197
|
+
const flags = /\bflags:\s+(\d+)\s*$/m.exec(debug);
|
|
198
|
+
if (!flags || flags[1] !== '0') {
|
|
199
|
+
throw new Error(`${label} "${path}" has non-zero Git index flags`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `path` is a repository-root-relative POSIX path. It must be exactly one HEAD
|
|
205
|
+
* blob, carry no index flags, and match its HEAD bytes and mode in the working
|
|
206
|
+
* tree. `label` names what the path IS in every refusal, so a reader can tell
|
|
207
|
+
* which checkout refused.
|
|
208
|
+
*/
|
|
209
|
+
function assertHeadBoundPath(trustedGit, repositoryRoot, path, label) {
|
|
210
|
+
const records = git(
|
|
211
|
+
trustedGit,
|
|
212
|
+
repositoryRoot,
|
|
213
|
+
['ls-tree', '-z', 'HEAD', '--', path],
|
|
214
|
+
'buffer',
|
|
215
|
+
)
|
|
216
|
+
.toString('utf8')
|
|
217
|
+
.split('\0')
|
|
218
|
+
.filter(Boolean);
|
|
219
|
+
if (records.length !== 1) {
|
|
220
|
+
throw new Error(`${label} "${path}" is not one exact HEAD blob`);
|
|
221
|
+
}
|
|
222
|
+
const { mode, objectId } = parseTreeRecord(records[0], path, label);
|
|
223
|
+
assertNormalIndexFlags(trustedGit, repositoryRoot, path, label);
|
|
224
|
+
|
|
225
|
+
const absolutePath = resolve(repositoryRoot, path);
|
|
226
|
+
assertContained(
|
|
227
|
+
repositoryRoot,
|
|
228
|
+
absolutePath,
|
|
229
|
+
`${label} "${path}"`,
|
|
230
|
+
`the checkout ${repositoryRoot}`,
|
|
231
|
+
);
|
|
232
|
+
assertSafeWorktreeFile(
|
|
233
|
+
absolutePath,
|
|
234
|
+
mode === '100755' ? 0o755 : 0o644,
|
|
235
|
+
`${label} "${path}"`,
|
|
236
|
+
);
|
|
237
|
+
const headBytes = git(
|
|
238
|
+
trustedGit,
|
|
239
|
+
repositoryRoot,
|
|
240
|
+
['cat-file', 'blob', objectId],
|
|
241
|
+
'buffer',
|
|
242
|
+
);
|
|
243
|
+
if (!readFileSync(absolutePath).equals(headBytes)) {
|
|
244
|
+
throw new Error(`${label} "${path}" does not match its HEAD blob`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Locate the Git checkout a root sits in. Locating it is separate from judging
|
|
250
|
+
* it — see {@link assertTrustedCheckoutLocation}.
|
|
251
|
+
*/
|
|
252
|
+
export function locateCheckout({ root, trustedGit, label }) {
|
|
253
|
+
assertPreparedExecutable(trustedGit, 'git');
|
|
254
|
+
let repositoryRoot;
|
|
255
|
+
try {
|
|
256
|
+
repositoryRoot = realpathSync(
|
|
257
|
+
git(trustedGit, root, ['rev-parse', '--show-toplevel']).trim(),
|
|
258
|
+
);
|
|
259
|
+
} catch {
|
|
260
|
+
throw new Error(`${label} verification requires a trusted Git checkout`);
|
|
261
|
+
}
|
|
262
|
+
const gitDirectory = realpathSync(
|
|
263
|
+
git(trustedGit, repositoryRoot, ['rev-parse', '--absolute-git-dir']).trim(),
|
|
264
|
+
);
|
|
265
|
+
return { repositoryRoot, gitDirectory };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* WHERE the checkout is: no ancestor may be writable by anyone but its owner or
|
|
270
|
+
* root, or another local process could swap the inputs between verification and
|
|
271
|
+
* use.
|
|
272
|
+
*
|
|
273
|
+
* This is a property of the MACHINE, not of the repository's contents, and the
|
|
274
|
+
* two are deliberately separately callable and separately tested: fused, every
|
|
275
|
+
* content test depended on the ambient environment (a runner whose `/home/<user>`
|
|
276
|
+
* is 0777 failed them all) and this rule, the one with real security weight, had
|
|
277
|
+
* no direct test. There is deliberately NO option to skip it in
|
|
278
|
+
* {@link gradeTrustedCheckout}.
|
|
279
|
+
*/
|
|
280
|
+
export function assertTrustedCheckoutLocation(
|
|
281
|
+
{ repositoryRoot, gitDirectory },
|
|
282
|
+
label,
|
|
283
|
+
) {
|
|
284
|
+
assertProtectedPathAncestry(repositoryRoot, `${label} checkout`);
|
|
285
|
+
assertProtectedPathAncestry(gitDirectory, `${label} Git directory`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* WHAT the checkout contains: an exact HEAD, every required path bound to its
|
|
290
|
+
* HEAD blob, and no untrusted working-tree change anywhere in the repository.
|
|
291
|
+
*
|
|
292
|
+
* `requirements` are `{root, label, requiredPaths}`: each `root` sits anywhere
|
|
293
|
+
* inside `repositoryRoot` and each required path is relative to its root, so it
|
|
294
|
+
* is re-rooted to the repository before git is asked. A path that resolves
|
|
295
|
+
* outside its root is refused BY NAME before git runs.
|
|
296
|
+
*/
|
|
297
|
+
export function verifyCheckoutContents({
|
|
298
|
+
repositoryRoot,
|
|
299
|
+
requirements,
|
|
300
|
+
trustedGit,
|
|
301
|
+
expectedCommit,
|
|
302
|
+
}) {
|
|
303
|
+
const commit = git(trustedGit, repositoryRoot, [
|
|
304
|
+
'rev-parse',
|
|
305
|
+
'--verify',
|
|
306
|
+
'HEAD^{commit}',
|
|
307
|
+
]).trim();
|
|
308
|
+
if (!COMMIT_PATTERN.test(commit)) {
|
|
309
|
+
throw new Error(`${repositoryRoot} HEAD is not an exact Git commit`);
|
|
310
|
+
}
|
|
311
|
+
if (expectedCommit !== undefined && commit !== expectedCommit) {
|
|
312
|
+
throw new Error(`${repositoryRoot} HEAD changed during verification`);
|
|
313
|
+
}
|
|
314
|
+
for (const { root, label, requiredPaths } of requirements) {
|
|
315
|
+
if (requiredPaths.length === 0) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`the ${label} checkout is graded for at least one path; none was named`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
for (const requiredPath of requiredPaths) {
|
|
321
|
+
const absolutePath = resolve(root, requiredPath);
|
|
322
|
+
assertContained(
|
|
323
|
+
root,
|
|
324
|
+
absolutePath,
|
|
325
|
+
`${label} path "${requiredPath}"`,
|
|
326
|
+
`the ${label} ${root}`,
|
|
327
|
+
);
|
|
328
|
+
assertHeadBoundPath(
|
|
329
|
+
trustedGit,
|
|
330
|
+
repositoryRoot,
|
|
331
|
+
relative(repositoryRoot, absolutePath).split(sep).join('/'),
|
|
332
|
+
label,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const unexpected = dirtyPaths(trustedGit, repositoryRoot).map(
|
|
337
|
+
(entry) => `${entry.status} ${entry.path}`,
|
|
338
|
+
);
|
|
339
|
+
if (unexpected.length > 0) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`checkout ${repositoryRoot} has untrusted source changes: ${unexpected.join(', ')}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return { repositoryRoot, commit };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Locate, judge the location of, and grade the contents of the checkout one
|
|
349
|
+
* root sits in. `root` and `requiredPaths` are as in {@link verifyCheckoutContents}.
|
|
350
|
+
*/
|
|
351
|
+
export function gradeTrustedCheckout({
|
|
352
|
+
root,
|
|
353
|
+
requiredPaths,
|
|
354
|
+
label,
|
|
355
|
+
trustedGit,
|
|
356
|
+
expectedCommit,
|
|
357
|
+
}) {
|
|
358
|
+
const canonicalRoot = realpathSync(root);
|
|
359
|
+
const location = locateCheckout({ root: canonicalRoot, trustedGit, label });
|
|
360
|
+
assertTrustedCheckoutLocation(location, label);
|
|
361
|
+
return verifyCheckoutContents({
|
|
362
|
+
repositoryRoot: location.repositoryRoot,
|
|
363
|
+
requirements: [{ root: canonicalRoot, label, requiredPaths }],
|
|
364
|
+
trustedGit,
|
|
365
|
+
expectedCommit,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Grade several roots, each repository ONCE: roots in the same repository are
|
|
371
|
+
* graded together under one HEAD and one clean-tree check. Returns a Map from
|
|
372
|
+
* each (canonical) root to `{repositoryRoot, commit}`. `expected` is an earlier
|
|
373
|
+
* result of this function; passing it demands every HEAD is unchanged.
|
|
374
|
+
*/
|
|
375
|
+
export function gradeTrustedCheckouts({ grades, trustedGit, expected }) {
|
|
376
|
+
const byRepository = new Map();
|
|
377
|
+
for (const { root, label, requiredPaths } of grades) {
|
|
378
|
+
const canonicalRoot = realpathSync(root);
|
|
379
|
+
const location = locateCheckout({ root: canonicalRoot, trustedGit, label });
|
|
380
|
+
const group = byRepository.get(location.repositoryRoot) ?? {
|
|
381
|
+
location,
|
|
382
|
+
labels: [],
|
|
383
|
+
requirements: [],
|
|
384
|
+
};
|
|
385
|
+
group.labels.push(label);
|
|
386
|
+
group.requirements.push({ root: canonicalRoot, label, requiredPaths });
|
|
387
|
+
byRepository.set(location.repositoryRoot, group);
|
|
388
|
+
}
|
|
389
|
+
const result = new Map();
|
|
390
|
+
for (const [repositoryRoot, group] of byRepository) {
|
|
391
|
+
assertTrustedCheckoutLocation(group.location, group.labels.join(' + '));
|
|
392
|
+
const graded = verifyCheckoutContents({
|
|
393
|
+
repositoryRoot,
|
|
394
|
+
requirements: group.requirements,
|
|
395
|
+
trustedGit,
|
|
396
|
+
expectedCommit: expected?.get(group.requirements[0].root)?.commit,
|
|
397
|
+
});
|
|
398
|
+
for (const { root } of group.requirements) {
|
|
399
|
+
result.set(root, graded);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* A verifier input (the evidence tree, the replay state) must live outside every
|
|
407
|
+
* graded checkout: a path inside one is graded as source, not as input.
|
|
408
|
+
*/
|
|
409
|
+
export function assertExternalInput(repositoryRoots, path, label) {
|
|
410
|
+
const canonical = realpathSync(path);
|
|
411
|
+
for (const repositoryRoot of repositoryRoots) {
|
|
412
|
+
const rel = relative(repositoryRoot, canonical);
|
|
413
|
+
if (rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..')) {
|
|
414
|
+
throw new Error(`${label} must be outside the checkout ${repositoryRoot}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return canonical;
|
|
418
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The lexical grammars the evidence plane checks, declared once. The kernel
|
|
2
|
+
// states the same shapes inside its schemas (`@xemahq/contracts` distribution
|
|
3
|
+
// schemas keep them module-private and export no pattern), so the evidence
|
|
4
|
+
// plane has no kernel export to import; this is the ONE CLI declaration every
|
|
5
|
+
// evidence module reads.
|
|
6
|
+
|
|
7
|
+
/** A full 40-hex lowercase git commit. */
|
|
8
|
+
export const COMMIT_PATTERN = /^[a-f0-9]{40}$/;
|
|
9
|
+
/** 64 lowercase hex digits, no algorithm prefix. */
|
|
10
|
+
export const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
11
|
+
/** An OCI/sigstore content digest: `sha256:` and 64 lowercase hex digits. */
|
|
12
|
+
export const SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
13
|
+
/** `owner/repository` of a GitHub repository. */
|
|
14
|
+
export const GITHUB_REPOSITORY_PATTERN = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
15
|
+
/** A GitHub branch or tag ref. */
|
|
16
|
+
export const GITHUB_REF_PATTERN = /^refs\/(?:heads|tags)\/.+$/;
|