@xemahq/xema 0.26.0 → 0.28.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.
Files changed (230) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/distribution-assets.manifest.json +1 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/dev.js +1 -0
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/down.d.ts.map +1 -1
  8. package/dist/commands/down.js +7 -1
  9. package/dist/commands/down.js.map +1 -1
  10. package/dist/commands/edition.d.ts +3 -0
  11. package/dist/commands/edition.d.ts.map +1 -0
  12. package/dist/commands/edition.js +46 -0
  13. package/dist/commands/edition.js.map +1 -0
  14. package/dist/commands/init.js +2 -2
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/serve.js +1 -0
  17. package/dist/commands/serve.js.map +1 -1
  18. package/dist/commands/up.js +1 -0
  19. package/dist/commands/up.js.map +1 -1
  20. package/dist/dev/env-defaults.d.ts.map +1 -1
  21. package/dist/dev/env-defaults.js +5 -2
  22. package/dist/dev/env-defaults.js.map +1 -1
  23. package/dist/dev/infra-preflight.d.ts +3 -2
  24. package/dist/dev/infra-preflight.d.ts.map +1 -1
  25. package/dist/dev/infra-preflight.js +18 -8
  26. package/dist/dev/infra-preflight.js.map +1 -1
  27. package/dist/edition/verify-image-evidence.bundle.mjs +54283 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2877 -0
  29. package/dist/runtime/config/schema.d.ts +1 -0
  30. package/dist/runtime/config/schema.d.ts.map +1 -1
  31. package/dist/runtime/custody/local-custody.d.ts +12 -0
  32. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  33. package/dist/runtime/custody/local-custody.js +22 -0
  34. package/dist/runtime/custody/local-custody.js.map +1 -0
  35. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  36. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  37. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  38. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  39. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  40. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  41. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  42. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  43. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  44. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  45. package/dist/runtime/custody/openbao-http-client.js +154 -0
  46. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  47. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  48. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  49. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  50. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  51. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  52. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  53. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  54. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  55. package/dist/runtime/edition/run-entry.d.ts +9 -0
  56. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  57. package/dist/runtime/edition/run-entry.js +36 -0
  58. package/dist/runtime/edition/run-entry.js.map +1 -0
  59. package/dist/runtime/edition/verb-map.d.ts +60 -0
  60. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  61. package/dist/runtime/edition/verb-map.js +146 -0
  62. package/dist/runtime/edition/verb-map.js.map +1 -0
  63. package/dist/runtime/env-seeder.d.ts +2 -0
  64. package/dist/runtime/env-seeder.d.ts.map +1 -1
  65. package/dist/runtime/env-seeder.js +18 -3
  66. package/dist/runtime/env-seeder.js.map +1 -1
  67. package/dist/runtime/launch.d.ts +2 -1
  68. package/dist/runtime/launch.d.ts.map +1 -1
  69. package/dist/runtime/launch.js +31 -7
  70. package/dist/runtime/launch.js.map +1 -1
  71. package/dist/runtime/paths.d.ts +1 -2
  72. package/dist/runtime/paths.d.ts.map +1 -1
  73. package/dist/runtime/paths.js +0 -10
  74. package/dist/runtime/paths.js.map +1 -1
  75. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  76. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  77. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  78. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  79. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  80. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  81. package/dist/runtime/runner/chart-preflight.js +30 -69
  82. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  83. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  84. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  85. package/dist/runtime/runner/chart-pull.js +96 -0
  86. package/dist/runtime/runner/chart-pull.js.map +1 -0
  87. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  88. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  89. package/dist/runtime/runner/chart-resolver.js +16 -62
  90. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  91. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  92. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  93. package/dist/runtime/runner/chart-set-lock.js +55 -99
  94. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  95. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  96. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  97. package/dist/runtime/runner/helm-cli.js +9 -7
  98. package/dist/runtime/runner/helm-cli.js.map +1 -1
  99. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  100. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  101. package/dist/runtime/runner/helm-runner.js +18 -16
  102. package/dist/runtime/runner/helm-runner.js.map +1 -1
  103. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  104. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  105. package/dist/runtime/sources/kinds/git.js +8 -5
  106. package/dist/runtime/sources/kinds/git.js.map +1 -1
  107. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  108. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  109. package/dist/runtime/sources/manifest-walk.js +8 -3
  110. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  111. package/dist/runtime/sources/schema.d.ts +3 -0
  112. package/dist/runtime/sources/schema.d.ts.map +1 -1
  113. package/dist/runtime/sources/schema.js +6 -1
  114. package/dist/runtime/sources/schema.js.map +1 -1
  115. package/dist/runtime/supervisor.d.ts +2 -0
  116. package/dist/runtime/supervisor.d.ts.map +1 -1
  117. package/dist/runtime/supervisor.js +1 -0
  118. package/dist/runtime/supervisor.js.map +1 -1
  119. package/edition/README.md +313 -0
  120. package/edition/assemble-producer-input.mjs +1251 -0
  121. package/edition/authorization-base.mjs +347 -0
  122. package/edition/biome-content.mjs +268 -0
  123. package/edition/build-biome-index.mjs +175 -0
  124. package/edition/build-platform-service-index.mjs +288 -0
  125. package/edition/build-runtime-image-source-index.mjs +821 -0
  126. package/edition/built-verifier.mjs +55 -0
  127. package/edition/catalogue-edition.mjs +49 -0
  128. package/edition/catalogue-fetch.mjs +64 -0
  129. package/edition/catalogue.mjs +135 -0
  130. package/edition/chart-set-source-commit.mjs +92 -0
  131. package/edition/check-committed-image-evidence.mjs +383 -0
  132. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  133. package/edition/comment-blanker.mjs +75 -0
  134. package/edition/connector-source-authority.mjs +60 -0
  135. package/edition/customer-definition.mjs +183 -0
  136. package/edition/customer-flags.mjs +7 -0
  137. package/edition/develop-customer-edition.mjs +99 -0
  138. package/edition/develop-edition-resolve.mjs +65 -0
  139. package/edition/develop-edition.mjs +274 -0
  140. package/edition/develop-freeze.mjs +173 -0
  141. package/edition/develop-image-coordinate.mjs +54 -0
  142. package/edition/develop-seal.mjs +77 -0
  143. package/edition/develop-snapshot-inputs.mjs +104 -0
  144. package/edition/dist-check.mjs +986 -0
  145. package/edition/distribution-evidence-set.mjs +987 -0
  146. package/edition/distribution-paths.mjs +191 -0
  147. package/edition/edition-artifact.mjs +284 -0
  148. package/edition/edition-biome-manifests.mjs +85 -0
  149. package/edition/edition-channel.mjs +6 -0
  150. package/edition/edition-chart-set.mjs +93 -0
  151. package/edition/edition-config.mjs +205 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +30 -0
  154. package/edition/edition-list.mjs +35 -0
  155. package/edition/edition-locks.mjs +69 -0
  156. package/edition/edition-protocol.mjs +228 -0
  157. package/edition/edition-workspace.mjs +187 -0
  158. package/edition/evidence-checkout.mjs +439 -0
  159. package/edition/evidence-grammars.mjs +16 -0
  160. package/edition/evidence-trust-pin.mjs +304 -0
  161. package/edition/evidence-trust-policy.mjs +294 -0
  162. package/edition/external-source-walk.mjs +463 -0
  163. package/edition/first-party-source-ids.mjs +59 -0
  164. package/edition/fleet-coordinate-finality.mjs +350 -0
  165. package/edition/git-source-auth.mjs +52 -0
  166. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  167. package/edition/image-evidence-replay-state.mjs +242 -0
  168. package/edition/image-evidence.mjs +1233 -0
  169. package/edition/kernel-contract-authority.mjs +443 -0
  170. package/edition/lib.mjs +3021 -0
  171. package/edition/no-follow-file.mjs +94 -0
  172. package/edition/oci-evidence-verbs.mjs +392 -0
  173. package/edition/oci-image-source-evidence.mjs +239 -0
  174. package/edition/oras.mjs +648 -0
  175. package/edition/pin-source.mjs +227 -0
  176. package/edition/plan-image-evidence-refresh.mjs +408 -0
  177. package/edition/platform-lock-readability.mjs +892 -0
  178. package/edition/platform-service-class.mjs +18 -0
  179. package/edition/platform-service-source-authority.mjs +105 -0
  180. package/edition/positive-closure.mjs +162 -0
  181. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  182. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  183. package/edition/produce-image-source-evidence.mjs +765 -0
  184. package/edition/producer-evidence-bundle.mjs +157 -0
  185. package/edition/registry-credentials.mjs +193 -0
  186. package/edition/release-catalogue.mjs +39 -0
  187. package/edition/release-channel-guard.mjs +109 -0
  188. package/edition/release-edition-artifact.mjs +157 -0
  189. package/edition/remedy-command.mjs +89 -0
  190. package/edition/repin-evidence-trust.mjs +101 -0
  191. package/edition/required-resource-provider.mjs +58 -0
  192. package/edition/resolve-chart-set.mjs +415 -0
  193. package/edition/resolve-customer-edition.mjs +288 -0
  194. package/edition/resolve-distribution.mjs +2318 -0
  195. package/edition/resolve-edition.mjs +186 -0
  196. package/edition/resolve-images.mjs +1606 -0
  197. package/edition/seal-image-digests.mjs +855 -0
  198. package/edition/sealed-evidence-accept.mjs +82 -0
  199. package/edition/source-authority.mjs +725 -0
  200. package/edition/source-closure.mjs +116 -0
  201. package/edition/strict-json.mjs +121 -0
  202. package/edition/string-order.mjs +5 -0
  203. package/edition/trust-policy-arming.mjs +120 -0
  204. package/edition/trusted-executable.mjs +166 -0
  205. package/edition/verifier-binding.mjs +45 -0
  206. package/edition/verifier-files.mjs +8 -0
  207. package/edition/verifier-kernel-schemas.mjs +22 -0
  208. package/edition/verifier-provenance.mjs +64 -0
  209. package/edition/verify-image-evidence.mjs +458 -0
  210. package/edition/verify-image-platforms.mjs +495 -0
  211. package/edition/why.mjs +56 -0
  212. package/edition/workspace-sections.mjs +195 -0
  213. package/examples/06-biome-sources.yaml +1 -0
  214. package/package.json +14 -6
  215. package/dist/commands/dist.d.ts +0 -16
  216. package/dist/commands/dist.d.ts.map +0 -1
  217. package/dist/commands/dist.js +0 -182
  218. package/dist/commands/dist.js.map +0 -1
  219. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  220. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  221. package/dist/runtime/distribution/available-biome.js +0 -156
  222. package/dist/runtime/distribution/available-biome.js.map +0 -1
  223. package/dist/runtime/distribution/compose.d.ts +0 -23
  224. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  225. package/dist/runtime/distribution/compose.js +0 -48
  226. package/dist/runtime/distribution/compose.js.map +0 -1
  227. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  228. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  229. package/dist/runtime/runner/chart-trust.js +0 -270
  230. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // `xema edition source-closure`: the source tree hash of one directory of a
3
+ // source repository at an exact commit, with its workspace-package closure.
4
+ //
5
+ // usage: xema edition source-closure --repo <repo> --path <dir> --commit <40 hex> --sources-root <dir> --source-hash <dir> --kernel <dir>
6
+ //
7
+ // These are the facts a content coordinate is derived from. A consumer frames
8
+ // them into its own coordinate; it never reads a git tree, a workspace graph
9
+ // or a lockfile itself. `--path` is relative to the repository (`.` for its
10
+ // root). The hashes are the ones the image content hash is built from, so a
11
+ // coordinate derived from them moves exactly when the built bytes can move.
12
+
13
+ import { join } from 'node:path';
14
+
15
+ import { parseEvidenceCliFlags } from './distribution-evidence-set.mjs';
16
+ import {
17
+ WorkspaceRoot,
18
+ configureEditionWorkspaceFromArgv,
19
+ isEntryModule,
20
+ sourcesRoot,
21
+ } from './edition-workspace.mjs';
22
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
23
+ import {
24
+ compareStrings,
25
+ gitCommitTreeIndex,
26
+ hashGitSourceTree,
27
+ serializeJson,
28
+ workspaceClosure,
29
+ workspacePackagesAtCommit,
30
+ } from './lib.mjs';
31
+
32
+ /** The tree hash algorithm of every hash this verb prints. */
33
+ export const SOURCE_CLOSURE_TREE_ALGORITHM = 'xema-source-tree-sha256-v2';
34
+ const LOCKFILE_PATH = 'pnpm-lock.yaml';
35
+
36
+ function assertContainedDirectory(directory) {
37
+ if (
38
+ typeof directory !== 'string' ||
39
+ directory.length === 0 ||
40
+ directory.startsWith('/') ||
41
+ (directory !== '.' && directory.split('/').some((part) => part === '' || part === '.' || part === '..'))
42
+ ) {
43
+ throw new Error(`--path "${String(directory)}" is not a directory inside the repository`);
44
+ }
45
+ return directory;
46
+ }
47
+
48
+ /**
49
+ * The closure of `directory` in `repoDir` at `commit`. `lockfileBlobId` is the
50
+ * repository lockfile's blob when the directory is buildable (it has a
51
+ * `package.json`), and `null` when it is not; a buildable directory in a
52
+ * repository with no lockfile is refused, because its build would resolve
53
+ * dependencies unpinned.
54
+ */
55
+ export function readSourceClosure({ repoDir, repo, directory, commit }) {
56
+ if (!COMMIT_PATTERN.test(commit ?? '')) {
57
+ throw new Error(`--commit must be an exact 40-hex commit (got ${JSON.stringify(commit ?? null)})`);
58
+ }
59
+ assertContainedDirectory(directory);
60
+ const tree = gitCommitTreeIndex(repoDir, commit);
61
+ const graph = workspacePackagesAtCommit(repoDir, commit, tree);
62
+ const buildable = tree.isFile(directory === '.' ? 'package.json' : `${directory}/package.json`);
63
+ const lockfileBlobId = buildable ? tree.blobId(LOCKFILE_PATH) : undefined;
64
+ if (buildable && lockfileBlobId === undefined) {
65
+ throw new Error(
66
+ `${repo}:${directory} has a package.json but ${repo} has no ${LOCKFILE_PATH} at ${commit.slice(0, 12)}`,
67
+ );
68
+ }
69
+ return {
70
+ schemaVersion: 1,
71
+ repo,
72
+ directory,
73
+ commit,
74
+ treeAlgorithm: SOURCE_CLOSURE_TREE_ALGORITHM,
75
+ treeHash: hashGitSourceTree(repoDir, directory, commit, SOURCE_CLOSURE_TREE_ALGORITHM),
76
+ buildable,
77
+ lockfileBlobId: lockfileBlobId ?? null,
78
+ closure: [...workspaceClosure(directory, graph, commit)].sort(compareStrings).map((member) => ({
79
+ directory: member,
80
+ treeHash: hashGitSourceTree(repoDir, member, commit, SOURCE_CLOSURE_TREE_ALGORITHM),
81
+ })),
82
+ };
83
+ }
84
+
85
+ function main() {
86
+ const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
87
+ WorkspaceRoot.Sources,
88
+ WorkspaceRoot.SourceHash,
89
+ // The hash implementation is held to the kernel it was built for.
90
+ WorkspaceRoot.Kernel,
91
+ ]);
92
+ const flags = parseEvidenceCliFlags(rest, { single: ['repo', 'path', 'commit'] });
93
+ const repo = flags.require('repo');
94
+ if (repo.startsWith('/') || repo.split('/').some((part) => part === '' || part === '.' || part === '..')) {
95
+ throw new Error(`--repo "${repo}" is not a repository under the sources root`);
96
+ }
97
+ process.stdout.write(
98
+ serializeJson(
99
+ readSourceClosure({
100
+ repoDir: join(sourcesRoot(), repo),
101
+ repo,
102
+ directory: flags.require('path'),
103
+ commit: flags.require('commit'),
104
+ }),
105
+ ),
106
+ );
107
+ }
108
+
109
+ if (isEntryModule(import.meta.url)) {
110
+ try {
111
+ main();
112
+ } catch (error) {
113
+ console.error(`[source-closure] ${error.message}`);
114
+ process.exitCode = 1;
115
+ }
116
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The ONE strict JSON reader for authored inputs of the distribution plane.
3
+ *
4
+ * `JSON.parse` resolves a duplicated object key by silently keeping the LAST
5
+ * occurrence. For an authored manifest that is a declaration being deleted
6
+ * with no signal: `xema-biome.json` once carried two `resources` keys inside
7
+ * one component, every reader dropped the first, the sealed lock never
8
+ * admitted the Redis namespace it declared, and the service crashlooped in a
9
+ * deployment on `redis-namespace-not-declared`. Nothing between the author and
10
+ * the cluster could say so, because every reader agreed on the wrong answer.
11
+ *
12
+ * So a duplicate key is REFUSED here, naming the file, the JSON pointer of the
13
+ * object and the key. There is deliberately no lenient mode and no "last wins"
14
+ * option: two values for one key is an authoring error with no correct reading.
15
+ *
16
+ * Node builtins only, so it runs from a bare checkout. The sibling
17
+ * implementation for the biome authoring toolchain is `strict-json.ts` in
18
+ * `@xemahq/biome-sdk` (xema-biome-sdk, `packages/sdks/biome-sdk/src/builder/lib/`);
19
+ * the two repositories share no package this could live in without a publish,
20
+ * so each holds one copy. Change both together.
21
+ */
22
+ import { readFileSync } from 'node:fs';
23
+
24
+ /** A JSON object declared the same key more than once. */
25
+ export class DuplicateJsonKeyError extends Error {
26
+ /**
27
+ * @param {string} source file path (or other handle) of the document
28
+ * @param {string} pointer RFC 6901 pointer of the OBJECT holding the key ("" = root)
29
+ * @param {string} key the duplicated key, after escape decoding
30
+ */
31
+ constructor(source, pointer, key) {
32
+ super(
33
+ `${source}: duplicate key ${JSON.stringify(key)} in the object at JSON pointer ` +
34
+ `${JSON.stringify(pointer)}${pointer === '' ? ' (the document root)' : ''}. ` +
35
+ `JSON.parse would silently keep the last occurrence and drop the other; ` +
36
+ `merge the two declarations into one key.`,
37
+ );
38
+ this.name = 'DuplicateJsonKeyError';
39
+ this.code = 'DUPLICATE_JSON_KEY';
40
+ this.source = source;
41
+ this.pointer = pointer;
42
+ this.key = key;
43
+ }
44
+ }
45
+
46
+ function pointerSegment(segment) {
47
+ return String(segment).replaceAll('~', '~0').replaceAll('/', '~1');
48
+ }
49
+
50
+ /**
51
+ * Scan a document `JSON.parse` has ALREADY accepted for duplicated keys.
52
+ *
53
+ * Because the grammar is known to be valid, the scan only has to tell a string
54
+ * from structure: a `{`, `[`, `,` or `"` inside a string is never structural,
55
+ * and a string is left only at an unescaped `"`. Keys are compared after
56
+ * escape decoding (`"a"` and `"a"` are the same key to every reader).
57
+ */
58
+ function assertNoDuplicateKeys(text, source) {
59
+ /** @type {Array<{object: boolean, pointer: string, keys: Set<string>, expectKey: boolean, key: string, index: number}>} */
60
+ const stack = [];
61
+ const childPointer = () => {
62
+ const top = stack.at(-1);
63
+ if (!top) {return '';}
64
+ return `${top.pointer}/${pointerSegment(top.object ? top.key : top.index)}`;
65
+ };
66
+ for (let i = 0; i < text.length; i += 1) {
67
+ const char = text[i];
68
+ if (char === '"') {
69
+ const start = i;
70
+ i += 1;
71
+ while (text[i] !== '"') {i += text[i] === '\\' ? 2 : 1;}
72
+ const top = stack.at(-1);
73
+ if (top?.object && top.expectKey) {
74
+ const key = JSON.parse(text.slice(start, i + 1));
75
+ if (top.keys.has(key)) {
76
+ throw new DuplicateJsonKeyError(source, top.pointer, key);
77
+ }
78
+ top.keys.add(key);
79
+ top.key = key;
80
+ top.expectKey = false;
81
+ }
82
+ } else if (char === '{' || char === '[') {
83
+ stack.push({
84
+ object: char === '{',
85
+ pointer: childPointer(),
86
+ keys: new Set(),
87
+ expectKey: char === '{',
88
+ key: '',
89
+ index: 0,
90
+ });
91
+ } else if (char === '}' || char === ']') {
92
+ stack.pop();
93
+ } else if (char === ',') {
94
+ const top = stack.at(-1);
95
+ if (top.object) {top.expectKey = true;}
96
+ else {top.index += 1;}
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Parse JSON text, refusing a duplicated object key anywhere in the document.
103
+ * `source` names the document in every error.
104
+ */
105
+ export function parseStrictJson(text, source) {
106
+ let value;
107
+ try {
108
+ value = JSON.parse(text);
109
+ } catch (error) {
110
+ throw new Error(`${source}: invalid JSON: ${error.message}`, {
111
+ cause: error,
112
+ });
113
+ }
114
+ assertNoDuplicateKeys(text, source);
115
+ return value;
116
+ }
117
+
118
+ /** Read and strictly parse a JSON file from the working tree. */
119
+ export function readStrictJsonFile(path) {
120
+ return parseStrictJson(readFileSync(path, 'utf8'), path);
121
+ }
@@ -0,0 +1,5 @@
1
+ /** Stable, locale-independent string comparator (code-point order). */
2
+ export function compareStrings(a, b) {
3
+ if (a === b) {return 0;}
4
+ return a < b ? -1 : 1;
5
+ }
@@ -0,0 +1,120 @@
1
+ // Arming assertion for the offline Sigstore trust policy.
2
+ //
3
+ // The policy a data root declares (`evidenceTrustPolicy`) is schema-valid while
4
+ // authorizing nobody: `policies`, `trustedCosignBinaryDigests` and
5
+ // `trustedGitBinaryDigests` all default to `[]` and the zod schema has no
6
+ // `.min(1)` on any of them. So the file parses green while the delivery plane
7
+ // remains deliberately disabled. That is a valid bootstrap state only while all
8
+ // three authority collections are empty. A partially populated policy is
9
+ // neither disabled nor usable and must fail `xema edition check`; delivery
10
+ // itself always requires the fully armed state.
11
+ //
12
+ // It is deliberately not a lint of taste. Each requirement below names a place
13
+ // where an empty list makes verification impossible, not merely weaker:
14
+ //
15
+ // policies[] -> `image-evidence.mjs` throws "offline trust
16
+ // policy authorizes no signer for <predicate>
17
+ // on channel <channel>" before any cosign call.
18
+ // trustedCosignBinaryDigests -> `trusted-executable.mjs` compares the cosign
19
+ // binary digest with `[].includes(...)`, which
20
+ // is false for every possible binary.
21
+ // trustedGitBinaryDigests -> same, for the git binary that pins the
22
+ // graded checkouts.
23
+ //
24
+ // The three predicate types are checked separately because a policy can be
25
+ // non-empty and still be unarmed for a stage: the verifier resolves candidates
26
+ // per predicate type, so authorizing only the producer identity leaves the
27
+ // completeness and entitlement stages throwing the same "authorizes no signer"
28
+ // at delivery time - armed-looking, and inert exactly where it matters.
29
+ //
30
+ // ARMING IS JUDGED FOR ONE CHANNEL: the channel of the edition being checked,
31
+ // sealed or verified. A release verification is only ever satisfied by a
32
+ // release signer and a develop one by a develop signer, so the signers of one
33
+ // channel neither arm nor disarm the other: a policy that authorizes the three
34
+ // stages on the release channel and pins both binaries is ARMED for a release
35
+ // edition whatever it says about develop.
36
+
37
+ export const TrustPolicyArmingState = Object.freeze({
38
+ ARMED: 'armed',
39
+ UNARMED: 'unarmed',
40
+ PARTIAL: 'partial',
41
+ });
42
+
43
+ /** Arming has no channel-less answer: `channel` is one of the kernel's evidence channels. */
44
+ export function assertArmingChannel(schemas, channel) {
45
+ if (!schemas.EVIDENCE_CHANNELS.includes(channel)) {
46
+ throw new Error(
47
+ `trust-policy arming is judged for one channel; got ${JSON.stringify(channel)}, expected ${schemas.EVIDENCE_CHANNELS.join(' | ')}`,
48
+ );
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Assess whether a parsed trust policy can authorize anything on `channel`.
54
+ *
55
+ * Returns `{ state, armed, unmet }`. `unmet` is empty exactly when `armed` is
56
+ * true. `unarmed` means the channel has no signer at all in a policy that is
57
+ * otherwise coherent: the exact, explicit bootstrap state where every authority
58
+ * collection is empty, or a policy whose signers all belong to another channel.
59
+ * Anything else that is not armed is `partial`. Each unmet entry names the
60
+ * field and what makes verification impossible, so the failure is actionable
61
+ * without reading the verifier.
62
+ */
63
+ export function assessTrustPolicyArming(policy, schemas, channel) {
64
+ assertArmingChannel(schemas, channel);
65
+ const unmet = [];
66
+ const stages = [
67
+ ['image-producer', schemas.IMAGE_PRODUCER_PREDICATE_TYPE],
68
+ ['distribution-completeness', schemas.DISTRIBUTION_IMAGES_PREDICATE_TYPE],
69
+ ['delivery-entitlement', schemas.IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE],
70
+ ];
71
+ const onChannel = policy.policies.filter((candidate) => candidate.channel === channel);
72
+ const authorized = stages.filter(([, predicateType]) => onChannel.some((candidate) => candidate.predicateTypes.includes(predicateType)));
73
+
74
+ if (policy.policies.length === 0) {
75
+ unmet.push(
76
+ 'policies[] is empty — no signer identity is authorized, so ' +
77
+ 'verifySigstoreEvidenceDescriptor throws "offline trust policy ' +
78
+ 'authorizes no signer" for every predicate before reading any bundle.',
79
+ );
80
+ } else {
81
+ for (const [stage, predicateType] of stages.filter((candidate) => !authorized.includes(candidate))) {
82
+ unmet.push(
83
+ `no policy authorizes the ${stage} predicate ${predicateType} on channel "${channel}" — ` +
84
+ `that stage of a ${channel} delivery would still throw "authorizes no signer" ` +
85
+ 'while the policy reads as armed.',
86
+ );
87
+ }
88
+ }
89
+
90
+ if (policy.trustedCosignBinaryDigests.length === 0) {
91
+ unmet.push(
92
+ 'trustedCosignBinaryDigests[] is empty — prepareDigestPinnedExecutable ' +
93
+ 'tests membership with [].includes(digest), which is false for every ' +
94
+ 'cosign binary, so no evidence can be verified on any host.',
95
+ );
96
+ }
97
+
98
+ if (policy.trustedGitBinaryDigests.length === 0) {
99
+ unmet.push(
100
+ 'trustedGitBinaryDigests[] is empty — the same membership test rejects ' +
101
+ 'every git binary, so no verified release checkout can be established.',
102
+ );
103
+ }
104
+
105
+ const armed = unmet.length === 0;
106
+ const bootstrap =
107
+ policy.policies.length === 0 &&
108
+ policy.trustedCosignBinaryDigests.length === 0 &&
109
+ policy.trustedGitBinaryDigests.length === 0;
110
+ // The channel states nothing while another channel does: its signers and the
111
+ // binary pins are that channel's arming, not half of this one's.
112
+ const anotherChannelOnly = onChannel.length === 0 && policy.policies.length > 0;
113
+ const state = armed
114
+ ? TrustPolicyArmingState.ARMED
115
+ : bootstrap || anotherChannelOnly
116
+ ? TrustPolicyArmingState.UNARMED
117
+ : TrustPolicyArmingState.PARTIAL;
118
+
119
+ return { state, armed, unmet };
120
+ }
@@ -0,0 +1,166 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import {
4
+ closeSync,
5
+ constants as fsConstants,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdtempSync,
9
+ openSync,
10
+ readFileSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { basename, dirname, isAbsolute, join } from 'node:path';
16
+
17
+ const PREPARED_EXECUTABLES = new WeakMap();
18
+
19
+ function sha256Digest(bytes) {
20
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
21
+ }
22
+
23
+ function validatePreparedExecutable(record, label) {
24
+ const stats = lstatSync(record.path);
25
+ if (!stats.isFile() || stats.isSymbolicLink()) {
26
+ throw new Error(
27
+ `prepared trusted ${label} must be a no-follow regular file`,
28
+ );
29
+ }
30
+ if (record.kind === 'root-owned') {
31
+ for (const field of ['dev', 'ino', 'uid', 'mode', 'size']) {
32
+ if (stats[field] !== record.snapshot[field]) {
33
+ throw new Error(`prepared trusted ${label} file identity changed`);
34
+ }
35
+ }
36
+ let current = record.path;
37
+ while (true) {
38
+ const currentStats = lstatSync(current);
39
+ if (currentStats.uid !== 0 || (currentStats.mode & 0o022) !== 0) {
40
+ throw new Error(
41
+ `${label} and every ancestor must be root-owned and not group/world writable`,
42
+ );
43
+ }
44
+ if (current === '/') {break;}
45
+ current = dirname(current);
46
+ }
47
+ if ((stats.mode & 0o777) !== 0o755) {
48
+ throw new Error(`prepared trusted ${label} must have exact mode 0755`);
49
+ }
50
+ }
51
+ if (sha256Digest(readFileSync(record.path)) !== record.digest) {
52
+ throw new Error(`prepared trusted ${label} bytes changed`);
53
+ }
54
+ }
55
+
56
+ export function prepareDigestPinnedExecutable({
57
+ executablePath,
58
+ trustedDigests,
59
+ label,
60
+ verifyVersion,
61
+ }) {
62
+ if (!isAbsolute(executablePath)) {
63
+ throw new Error(`${label} executable path must be absolute`);
64
+ }
65
+ const sourceStats = lstatSync(executablePath);
66
+ if (!sourceStats.isFile() || sourceStats.isSymbolicLink()) {
67
+ throw new Error(`${label} executable must be a no-follow regular file`);
68
+ }
69
+ const bytes = readFileSync(executablePath);
70
+ const digest = sha256Digest(bytes);
71
+ if (!trustedDigests.includes(digest)) {
72
+ throw new Error(
73
+ `${label} executable digest ${digest} is not trusted by policy`,
74
+ );
75
+ }
76
+ const directory = mkdtempSync(join(tmpdir(), `xema-trusted-${label}-`));
77
+ const privatePath = join(directory, basename(executablePath));
78
+ try {
79
+ let descriptor;
80
+ try {
81
+ descriptor = openSync(
82
+ privatePath,
83
+ fsConstants.O_CREAT |
84
+ fsConstants.O_EXCL |
85
+ fsConstants.O_WRONLY |
86
+ fsConstants.O_NOFOLLOW,
87
+ 0o500,
88
+ );
89
+ writeFileSync(descriptor, bytes);
90
+ fsyncSync(descriptor);
91
+ } finally {
92
+ if (descriptor !== undefined) {closeSync(descriptor);}
93
+ }
94
+ verifyVersion(privatePath);
95
+ } catch (error) {
96
+ rmSync(directory, { recursive: true, force: true });
97
+ throw error;
98
+ }
99
+ const handle = {
100
+ path: privatePath,
101
+ dispose() {
102
+ rmSync(directory, { recursive: true, force: true });
103
+ },
104
+ };
105
+ PREPARED_EXECUTABLES.set(handle, {
106
+ kind: 'private-copy',
107
+ path: privatePath,
108
+ digest,
109
+ });
110
+ return handle;
111
+ }
112
+
113
+ export function prepareRootOwnedPinnedExecutable({
114
+ executablePath,
115
+ trustedDigests,
116
+ label,
117
+ verifyVersion,
118
+ }) {
119
+ if (!isAbsolute(executablePath)) {
120
+ throw new Error(`${label} executable path must be absolute`);
121
+ }
122
+ const initialStats = lstatSync(executablePath);
123
+ if (!initialStats.isFile() || initialStats.isSymbolicLink()) {
124
+ throw new Error(`${label} executable must be a no-follow regular file`);
125
+ }
126
+ const handle = { path: executablePath, dispose() {} };
127
+ const record = {
128
+ kind: 'root-owned',
129
+ path: executablePath,
130
+ digest: sha256Digest(readFileSync(executablePath)),
131
+ snapshot: {
132
+ dev: initialStats.dev,
133
+ ino: initialStats.ino,
134
+ uid: initialStats.uid,
135
+ mode: initialStats.mode,
136
+ size: initialStats.size,
137
+ },
138
+ };
139
+ PREPARED_EXECUTABLES.set(handle, record);
140
+ validatePreparedExecutable(record, label);
141
+ if (!trustedDigests.includes(record.digest)) {
142
+ throw new Error(
143
+ `${label} executable digest ${record.digest} is not trusted by policy`,
144
+ );
145
+ }
146
+ verifyVersion(executablePath);
147
+ validatePreparedExecutable(record, label);
148
+ return handle;
149
+ }
150
+
151
+ export function assertPreparedExecutable(handle, label) {
152
+ const record = PREPARED_EXECUTABLES.get(handle);
153
+ if (!record) {
154
+ throw new Error(`a prepared trusted ${label} executable is required`);
155
+ }
156
+ validatePreparedExecutable(record, label);
157
+ return record.path;
158
+ }
159
+
160
+ export function exactExecutableOutput(path, args, options = {}) {
161
+ return execFileSync(path, args, {
162
+ encoding: 'utf8',
163
+ stdio: ['ignore', 'pipe', 'pipe'],
164
+ ...options,
165
+ });
166
+ }
@@ -0,0 +1,45 @@
1
+ // What the running evidence verifier is held to before it trusts anything:
2
+ // a clean launch environment, an install nobody else can write, and the
3
+ // data-root pin of its own bytes. (The kernel the workspace states is held to
4
+ // the bundled edition protocol by `edition-protocol.mjs`.)
5
+
6
+ import { readFileSync, realpathSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+
9
+ import { assertProtectedPathAncestry } from './evidence-checkout.mjs';
10
+ import { VERIFIER_PROVENANCE_FILE } from './verifier-files.mjs';
11
+ import { parseVerifierProvenance } from './verifier-provenance.mjs';
12
+
13
+ /** The launch environment carries no variable that changes what code runs. */
14
+ export function assertSanitizedOperatorEnvironment(env) {
15
+ for (const key of Object.keys(env)) {
16
+ if (
17
+ key === 'NODE_OPTIONS' ||
18
+ key === 'NODE_PATH' ||
19
+ key.startsWith('DYLD_') ||
20
+ key.startsWith('LD_') ||
21
+ key.startsWith('GIT_')
22
+ ) {
23
+ throw new Error(
24
+ `trusted verifier launch refuses inherited control variable ${key}`,
25
+ );
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The verifier file that is running (the realpath of `bundlePath`) and its
32
+ * provenance beside it, after refusing an install anyone else can write or
33
+ * that a foreign user owns, and a provenance that does not describe this
34
+ * bundle. Returns the bytes the pin is compared with.
35
+ */
36
+ export function readRunningVerifier(bundlePath) {
37
+ const realBundle = realpathSync(bundlePath);
38
+ assertProtectedPathAncestry(realBundle, 'the evidence verifier install');
39
+ const bundleBytes = readFileSync(realBundle);
40
+ const provenanceBytes = readFileSync(
41
+ join(dirname(realBundle), VERIFIER_PROVENANCE_FILE),
42
+ );
43
+ parseVerifierProvenance({ provenanceBytes, bundleBytes });
44
+ return { bundleBytes, provenanceBytes };
45
+ }
@@ -0,0 +1,8 @@
1
+ // The names and place of the built evidence verifier. Pure constants, shared by
2
+ // the build, the gate and the verifier itself (which is bundled and so must not
3
+ // derive anything from where it sits).
4
+
5
+ export const VERIFIER_BUNDLE_FILE = 'verify-image-evidence.bundle.mjs';
6
+ export const VERIFIER_PROVENANCE_FILE =
7
+ 'verify-image-evidence.bundle.provenance.json';
8
+ export const VERIFIER_DIST_SUBPATH = Object.freeze(['dist', 'edition']);
@@ -0,0 +1,22 @@
1
+ import { assertKernelEditionProtocol } from './edition-protocol.mjs';
2
+ import { kernelPackageDir } from './edition-workspace.mjs';
3
+ import { loadKernelDistributionSchemas } from './lib.mjs';
4
+
5
+ /**
6
+ * The ONE place the evidence verifier obtains the kernel distribution schemas,
7
+ * and the edition protocol they implement.
8
+ *
9
+ * From source it loads the built kernel the workspace states with `--kernel`,
10
+ * whose protocol is then trivially the stated one. The verifier build
11
+ * (`scripts/build-evidence-verifier.mjs`) replaces this module with the
12
+ * `@xemahq/contracts` distribution schemas and edition protocol identity
13
+ * bundled from the CLI's own dependency, so the bundle never executes a schema
14
+ * from outside itself; the entry then holds `--kernel`'s declared protocol
15
+ * equal to the bundled one.
16
+ */
17
+ export async function loadVerifierKernelSchemas() {
18
+ return {
19
+ schemas: await loadKernelDistributionSchemas(),
20
+ editionProtocol: assertKernelEditionProtocol(kernelPackageDir()),
21
+ };
22
+ }
@@ -0,0 +1,64 @@
1
+ // The provenance record written beside the evidence verifier bundle, and the
2
+ // one reader of it. The build writes it (`scripts/build-evidence-verifier.mjs`);
3
+ // the data root pins its digest beside the bundle's.
4
+ //
5
+ // Deterministic by construction: no timestamps, no host paths. Every bundled
6
+ // input is {package, version, path relative to ITS package root, sha256}, so the
7
+ // record does not depend on how the dependencies were installed.
8
+
9
+ import { sha256Digest } from './image-evidence.mjs';
10
+ import { compareStrings } from './string-order.mjs';
11
+
12
+ export const VERIFIER_PROVENANCE_SCHEMA_VERSION = 1;
13
+
14
+ const PROVENANCE_KEYS = Object.freeze([
15
+ 'artifactSha256',
16
+ 'esbuild',
17
+ 'inputs',
18
+ 'schemaVersion',
19
+ ]);
20
+ const INPUT_KEYS = Object.freeze(['package', 'path', 'sha256', 'version']);
21
+ const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
22
+
23
+ const exactKeys = (value, keys) =>
24
+ value !== null &&
25
+ typeof value === 'object' &&
26
+ !Array.isArray(value) &&
27
+ Object.keys(value).sort().join() === keys.join();
28
+
29
+ /** Total order of inputs: package, then path. */
30
+ export const compareProvenanceInputs = (a, b) =>
31
+ compareStrings(a.package, b.package) || compareStrings(a.path, b.path);
32
+
33
+ /** Parse provenance bytes strictly, binding them to the bundle they describe. */
34
+ export function parseVerifierProvenance({ provenanceBytes, bundleBytes }) {
35
+ const provenance = JSON.parse(provenanceBytes.toString('utf8'));
36
+ if (
37
+ !exactKeys(provenance, PROVENANCE_KEYS) ||
38
+ provenance.schemaVersion !== VERIFIER_PROVENANCE_SCHEMA_VERSION ||
39
+ !exactKeys(provenance.esbuild, ['version']) ||
40
+ typeof provenance.esbuild.version !== 'string' ||
41
+ !Array.isArray(provenance.inputs) ||
42
+ provenance.inputs.length === 0
43
+ ) {
44
+ throw new Error('verifier provenance has an invalid structure');
45
+ }
46
+ provenance.inputs.forEach((input, index) => {
47
+ if (
48
+ !exactKeys(input, INPUT_KEYS) ||
49
+ INPUT_KEYS.some((key) => typeof input[key] !== 'string' || input[key] === '') ||
50
+ !SHA256_DIGEST.test(input.sha256) ||
51
+ input.path.startsWith('/') ||
52
+ input.path.split('/').includes('..') ||
53
+ (index > 0 && compareProvenanceInputs(provenance.inputs[index - 1], input) >= 0)
54
+ ) {
55
+ throw new Error(
56
+ `verifier provenance input ${index} is malformed or out of order`,
57
+ );
58
+ }
59
+ });
60
+ if (provenance.artifactSha256 !== sha256Digest(bundleBytes)) {
61
+ throw new Error('verifier provenance does not describe this bundle');
62
+ }
63
+ return provenance;
64
+ }