@xemahq/edition-tooling 0.0.0-stage → 0.1.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 (219) hide show
  1. package/LICENSE +175 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +47 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/edition.d.ts +4 -0
  7. package/dist/commands/edition.d.ts.map +1 -0
  8. package/dist/commands/edition.js +58 -0
  9. package/dist/commands/edition.js.map +1 -0
  10. package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
  11. package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
  12. package/dist/lib/exit-codes.d.ts +6 -0
  13. package/dist/lib/exit-codes.d.ts.map +1 -0
  14. package/dist/lib/exit-codes.js +21 -0
  15. package/dist/lib/exit-codes.js.map +1 -0
  16. package/dist/runtime/edition/run-entry.d.ts +10 -0
  17. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  18. package/dist/runtime/edition/run-entry.js +49 -0
  19. package/dist/runtime/edition/run-entry.js.map +1 -0
  20. package/dist/runtime/edition/verb-map.d.ts +70 -0
  21. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  22. package/dist/runtime/edition/verb-map.js +166 -0
  23. package/dist/runtime/edition/verb-map.js.map +1 -0
  24. package/edition/README.md +291 -0
  25. package/edition/adapter-bundled-versions.mjs +128 -0
  26. package/edition/adapter-host-versions.mjs +76 -0
  27. package/edition/assemble-producer-input.mjs +1423 -0
  28. package/edition/assemble-third-party-input.mjs +348 -0
  29. package/edition/authored-source-check.mjs +141 -0
  30. package/edition/authority-binding.mjs +23 -0
  31. package/edition/authorization-base.mjs +357 -0
  32. package/edition/biome-content.mjs +327 -0
  33. package/edition/build-biome-index.mjs +180 -0
  34. package/edition/build-ledger-completeness.mjs +67 -0
  35. package/edition/build-ledger.mjs +107 -0
  36. package/edition/build-platform-service-index.mjs +292 -0
  37. package/edition/build-runtime-image-source-index.mjs +1197 -0
  38. package/edition/built-verifier.mjs +55 -0
  39. package/edition/candidate-edition.mjs +735 -0
  40. package/edition/canonical-evidence-json.mjs +82 -0
  41. package/edition/canonical-repository-url.mjs +61 -0
  42. package/edition/catalogue-edition.mjs +49 -0
  43. package/edition/catalogue-evidence.mjs +228 -0
  44. package/edition/catalogue-fetch.mjs +124 -0
  45. package/edition/catalogue-lock-field.mjs +41 -0
  46. package/edition/catalogue-pin.mjs +71 -0
  47. package/edition/catalogue.mjs +237 -0
  48. package/edition/chart-set-source-commit.mjs +97 -0
  49. package/edition/check-committed-image-evidence.mjs +412 -0
  50. package/edition/checkout-indexed-source-repositories.mjs +283 -0
  51. package/edition/collected-refusals.mjs +59 -0
  52. package/edition/comment-blanker.mjs +75 -0
  53. package/edition/connector-source-authority.mjs +60 -0
  54. package/edition/contract-authority.mjs +122 -0
  55. package/edition/cosign-certificate-constraints.mjs +50 -0
  56. package/edition/cross-repo-build-inputs.mjs +158 -0
  57. package/edition/customer-definition.mjs +72 -0
  58. package/edition/customer-edition-verbs.mjs +610 -0
  59. package/edition/customer-flags.mjs +15 -0
  60. package/edition/definition-checkout.mjs +96 -0
  61. package/edition/develop-build-record.mjs +122 -0
  62. package/edition/develop-catalogue-free-resolve.mjs +282 -0
  63. package/edition/develop-catalogue-free-vendor.mjs +118 -0
  64. package/edition/develop-channel-evidence.mjs +23 -0
  65. package/edition/develop-channel-record.mjs +49 -0
  66. package/edition/develop-channel-satellites.mjs +65 -0
  67. package/edition/develop-chart-roster.mjs +96 -0
  68. package/edition/develop-chart-set.mjs +147 -0
  69. package/edition/develop-current-resolution.mjs +29 -0
  70. package/edition/develop-edition-invariants.mjs +142 -0
  71. package/edition/develop-edition-resolve.mjs +250 -0
  72. package/edition/develop-edition.mjs +332 -0
  73. package/edition/develop-engine-image.mjs +97 -0
  74. package/edition/develop-freeze.mjs +304 -0
  75. package/edition/develop-git-bundles.mjs +296 -0
  76. package/edition/develop-graph.mjs +34 -0
  77. package/edition/develop-image-coordinate.mjs +85 -0
  78. package/edition/develop-image-probe.mjs +122 -0
  79. package/edition/develop-image-record.mjs +260 -0
  80. package/edition/develop-resolve-report.mjs +159 -0
  81. package/edition/develop-select-records.mjs +90 -0
  82. package/edition/develop-selection-line.mjs +29 -0
  83. package/edition/develop-selection-pins.mjs +36 -0
  84. package/edition/develop-snapshot-inputs.mjs +112 -0
  85. package/edition/develop-store-root.mjs +20 -0
  86. package/edition/dispatched-runtime-components.mjs +172 -0
  87. package/edition/dist-check.mjs +1174 -0
  88. package/edition/distribution-evidence-set.mjs +1064 -0
  89. package/edition/distribution-paths.mjs +191 -0
  90. package/edition/edition-artifact.mjs +529 -0
  91. package/edition/edition-biome-manifests.mjs +85 -0
  92. package/edition/edition-ceiling.mjs +182 -0
  93. package/edition/edition-channel.mjs +27 -0
  94. package/edition/edition-chart-set.mjs +104 -0
  95. package/edition/edition-config.mjs +374 -0
  96. package/edition/edition-constants.mjs +110 -0
  97. package/edition/edition-contract-parse.mjs +179 -0
  98. package/edition/edition-contract.json +100 -0
  99. package/edition/edition-image-seal.mjs +115 -0
  100. package/edition/edition-kind.mjs +10 -0
  101. package/edition/edition-list.mjs +35 -0
  102. package/edition/edition-locks.mjs +78 -0
  103. package/edition/edition-pin-verbs.mjs +183 -0
  104. package/edition/edition-pin.mjs +17 -0
  105. package/edition/edition-pinned-inputs.mjs +116 -0
  106. package/edition/edition-protocol.mjs +285 -0
  107. package/edition/edition-publisher.mjs +21 -0
  108. package/edition/edition-seal.mjs +257 -0
  109. package/edition/edition-source-pins.mjs +211 -0
  110. package/edition/edition-unpin.mjs +16 -0
  111. package/edition/edition-workspace.mjs +205 -0
  112. package/edition/evidence-checkout.mjs +439 -0
  113. package/edition/evidence-grammars.mjs +18 -0
  114. package/edition/evidence-input-mode.mjs +136 -0
  115. package/edition/evidence-trust-pin.mjs +392 -0
  116. package/edition/evidence-trust-policy.mjs +374 -0
  117. package/edition/external-source-walk.mjs +486 -0
  118. package/edition/first-party-source-ids.mjs +62 -0
  119. package/edition/fleet-coordinate-finality.mjs +363 -0
  120. package/edition/fulcio-certificate-extensions.mjs +278 -0
  121. package/edition/git-json.mjs +54 -0
  122. package/edition/git-source-auth.mjs +131 -0
  123. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  124. package/edition/image-evidence-replay-state.mjs +242 -0
  125. package/edition/image-evidence.mjs +1328 -0
  126. package/edition/image-lock-service-rows.mjs +26 -0
  127. package/edition/image-subject-identity.mjs +19 -0
  128. package/edition/images-from-record.mjs +216 -0
  129. package/edition/indexes-at-pins.mjs +13 -0
  130. package/edition/infra-images-lock.mjs +105 -0
  131. package/edition/kernel-capability-domain.mjs +30 -0
  132. package/edition/kernel-contract-authority.mjs +526 -0
  133. package/edition/kernel-contract-source-path-set.mjs +96 -0
  134. package/edition/kernel-package.mjs +237 -0
  135. package/edition/kernel-pin-source.mjs +63 -0
  136. package/edition/kernel-protocol-choice.mjs +239 -0
  137. package/edition/kernel-signer-kind.mjs +23 -0
  138. package/edition/lib.mjs +3322 -0
  139. package/edition/locked-bundle-signer.mjs +297 -0
  140. package/edition/map-with-concurrency.mjs +19 -0
  141. package/edition/no-follow-file.mjs +94 -0
  142. package/edition/oci-evidence-verbs.mjs +421 -0
  143. package/edition/oci-image-source-evidence.mjs +377 -0
  144. package/edition/operation-grant-admission.mjs +285 -0
  145. package/edition/operation-requirements.mjs +282 -0
  146. package/edition/oras-process.mjs +35 -0
  147. package/edition/oras.mjs +664 -0
  148. package/edition/pin-source.mjs +242 -0
  149. package/edition/pinned-cosign.mjs +86 -0
  150. package/edition/plan-image-evidence-refresh.mjs +428 -0
  151. package/edition/platform-lock-readability.mjs +903 -0
  152. package/edition/platform-service-class.mjs +85 -0
  153. package/edition/platform-service-ownership.mjs +91 -0
  154. package/edition/platform-service-source-authority.mjs +105 -0
  155. package/edition/positive-closure.mjs +162 -0
  156. package/edition/previous-image-lock.mjs +25 -0
  157. package/edition/produce-distribution-completeness-evidence.mjs +317 -0
  158. package/edition/produce-edition-catalogue-evidence.mjs +252 -0
  159. package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
  160. package/edition/produce-image-source-evidence.mjs +788 -0
  161. package/edition/producer-evidence-bundle.mjs +201 -0
  162. package/edition/producer-evidence-index-file.mjs +4 -0
  163. package/edition/record-refused.mjs +11 -0
  164. package/edition/registry-client-factory.mjs +21 -0
  165. package/edition/registry-credentials.mjs +225 -0
  166. package/edition/release-catalogue.mjs +48 -0
  167. package/edition/release-channel-guard.mjs +108 -0
  168. package/edition/release-edition-artifact.mjs +152 -0
  169. package/edition/release-edition.mjs +54 -0
  170. package/edition/remedy-command.mjs +89 -0
  171. package/edition/repin-evidence-trust.mjs +109 -0
  172. package/edition/required-resource-provider.mjs +58 -0
  173. package/edition/resolve-chart-set.mjs +376 -0
  174. package/edition/resolve-customer-edition.mjs +374 -0
  175. package/edition/resolve-distribution.mjs +2642 -0
  176. package/edition/resolve-edition.mjs +192 -0
  177. package/edition/resolve-images.mjs +2344 -0
  178. package/edition/resolve-infra-images.mjs +88 -0
  179. package/edition/seal-image-digests.mjs +954 -0
  180. package/edition/sealed-distribution-lock.mjs +22 -0
  181. package/edition/sealed-evidence-accept.mjs +82 -0
  182. package/edition/shallow-checkout.mjs +18 -0
  183. package/edition/sigstore-bundle-media-type.mjs +4 -0
  184. package/edition/source-authority.mjs +713 -0
  185. package/edition/source-closure.mjs +115 -0
  186. package/edition/store-publisher-trust.mjs +40 -0
  187. package/edition/strict-json.mjs +121 -0
  188. package/edition/string-order.mjs +5 -0
  189. package/edition/third-party-admission.mjs +118 -0
  190. package/edition/third-party-bundle-registry.mjs +349 -0
  191. package/edition/third-party-certificate-extensions.mjs +58 -0
  192. package/edition/third-party-image-pins.mjs +41 -0
  193. package/edition/third-party-pins-trusted.mjs +24 -0
  194. package/edition/third-party-producer-evidence.mjs +76 -0
  195. package/edition/third-party-source-check.mjs +28 -0
  196. package/edition/third-party-source-context.mjs +41 -0
  197. package/edition/third-party-source-evidence.mjs +326 -0
  198. package/edition/third-party-source-flags.mjs +148 -0
  199. package/edition/third-party-source-pins.mjs +270 -0
  200. package/edition/third-party-source-uniqueness.mjs +54 -0
  201. package/edition/third-party-source-walk.mjs +118 -0
  202. package/edition/third-party-source-writer.mjs +228 -0
  203. package/edition/third-party-subjects.mjs +128 -0
  204. package/edition/trust-policy-arming-verb.mjs +60 -0
  205. package/edition/trust-policy-arming.mjs +144 -0
  206. package/edition/trust-policy-image-ownership.mjs +85 -0
  207. package/edition/trusted-executable.mjs +166 -0
  208. package/edition/verifier-binding.mjs +45 -0
  209. package/edition/verifier-digest-pin.mjs +17 -0
  210. package/edition/verifier-files.mjs +8 -0
  211. package/edition/verifier-kernel-schemas.mjs +22 -0
  212. package/edition/verifier-provenance.mjs +64 -0
  213. package/edition/verify-catalogue-evidence.mjs +247 -0
  214. package/edition/verify-image-evidence.mjs +493 -0
  215. package/edition/verify-image-platforms.mjs +523 -0
  216. package/edition/why.mjs +56 -0
  217. package/edition/workspace-sections.mjs +287 -0
  218. package/package.json +83 -4
  219. package/README.md +0 -3
@@ -0,0 +1,115 @@
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> --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
+ // The hash implementation is held to the kernel it was built for.
89
+ WorkspaceRoot.Kernel,
90
+ ]);
91
+ const flags = parseEvidenceCliFlags(rest, { single: ['repo', 'path', 'commit'] });
92
+ const repo = flags.require('repo');
93
+ if (repo.startsWith('/') || repo.split('/').some((part) => part === '' || part === '.' || part === '..')) {
94
+ throw new Error(`--repo "${repo}" is not a repository under the sources root`);
95
+ }
96
+ process.stdout.write(
97
+ serializeJson(
98
+ readSourceClosure({
99
+ repoDir: join(sourcesRoot(), repo),
100
+ repo,
101
+ directory: flags.require('path'),
102
+ commit: flags.require('commit'),
103
+ }),
104
+ ),
105
+ );
106
+ }
107
+
108
+ if (isEntryModule(import.meta.url)) {
109
+ try {
110
+ main();
111
+ } catch (error) {
112
+ console.error(`[source-closure] ${error.message}`);
113
+ process.exitCode = 1;
114
+ }
115
+ }
@@ -0,0 +1,40 @@
1
+ // The Store publishers a vendor edition's data root trusts: read from
2
+ // `store-publisher-identities.json` and written to the lock as
3
+ // `storePublisherTrust`. The kernel lock schema is the ONE validator (non-empty,
4
+ // sorted by anchorKey, unique, https issuer, no refs/pull/); it runs at the
5
+ // resolver's `parseUnsealedDistributionLock` step, so nothing here re-checks it.
6
+
7
+ import { existsSync } from 'node:fs';
8
+
9
+ import { storePublisherIdentitiesPath } from './edition-workspace.mjs';
10
+ import { readStrictJsonFile } from './strict-json.mjs';
11
+
12
+ /** The data root's trusted Store publishers, or `undefined` when it holds no such file. */
13
+ export function readStorePublisherTrust() {
14
+ const path = storePublisherIdentitiesPath();
15
+ if (!existsSync(path)) {return undefined;}
16
+ const document = readStrictJsonFile(path);
17
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
18
+ throw new Error(`${path}: must be a JSON object`);
19
+ }
20
+ const unknown = Object.keys(document).filter((key) => key !== 'schemaVersion' && key !== 'publishers');
21
+ if (unknown.length > 0) {
22
+ throw new Error(`${path}: unknown top-level key ${unknown.map((key) => JSON.stringify(key)).join(', ')}`);
23
+ }
24
+ if (document.schemaVersion === 1) {
25
+ throw new Error(
26
+ `${path}: schemaVersion 1 states no signer; move the file to schemaVersion 2 (\`oidcIssuer\` and \`identity\` per anchor)`,
27
+ );
28
+ }
29
+ if (document.schemaVersion !== 2) {
30
+ throw new Error(`${path}: schemaVersion must be 2, got ${JSON.stringify(document.schemaVersion)}`);
31
+ }
32
+ const { publishers } = document;
33
+ if (!Array.isArray(publishers) || publishers.some((row) => row === null || typeof row !== 'object' || Array.isArray(row))) {
34
+ throw new Error(`${path}: publishers must be an array of objects`);
35
+ }
36
+ // Only `sourceRepositories` (vendor-ops' key) is dropped; every other key reaches the kernel's
37
+ // strict schema unchanged, so a stray or misspelt key is refused there, never dropped here.
38
+ return publishers.map(({ sourceRepositories: _vendorOps, ...row }) => row);
39
+ }
40
+
@@ -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,118 @@
1
+ // Admission of third-party image subjects (XEMA-1611 design section 4), beside `externalSourceRepositoryPins` and
2
+ // `verifyDistributionImageEvidenceSet` of `image-evidence.mjs`. Pure over data: the pins come from the data root's
3
+ // `third-party-source-pins.json` (held to the evidence trust pin by the entry), the statement and bundle from a
4
+ // descriptor `verifySigstoreEvidenceDescriptor` has just verified.
5
+
6
+ import { assertCertificateExtensions } from './third-party-certificate-extensions.mjs';
7
+
8
+ /** The declared source kind that names a third-party pin. */
9
+ const OCI_SOURCE_KIND = 'oci';
10
+ const FIRST_PARTY_LAYOUT_PREFIX = /^(?:repos|submodules)\//u;
11
+
12
+ const repositoryPathOf = (repositoryUrl) => new URL(repositoryUrl).pathname.replace(/^\//u, '').replace(/\.git$/u, '');
13
+
14
+ /**
15
+ * The repository pins of the edition's declared OCI sources, derived from their third-party source pins:
16
+ * `repo` (the pinned `owner/name`), `repositoryUrl` and `sourceCommit`. Every declared OCI source must be pinned (a
17
+ * missing or empty pin file refuses, with the loader's reason), and the pin must not collide with an index pin or a git
18
+ * source, nor sit under the first-party layout (`repos/`, `submodules/`). A declared source that ships no image yields no
19
+ * pin; every subject of a source must agree on the pinned commit.
20
+ *
21
+ * @returns {Map<string, {repo: string, repositoryUrl: string, sourceCommit: string}>}
22
+ */
23
+ export function thirdPartySourceRepositoryPins({ distribution, imageLock, repositoryPins, thirdPartyPins }) {
24
+ const sources = distribution.sources ?? [];
25
+ const derived = new Map();
26
+ for (const source of sources) {
27
+ if (source.kind !== OCI_SOURCE_KIND) {continue;}
28
+ const pin = thirdPartyPins.byName(source.name);
29
+ if (pin === undefined) {
30
+ throw new Error(
31
+ `edition source "${source.name}" is kind "${OCI_SOURCE_KIND}", but ${thirdPartyPins.reason ?? `the data root's third-party source pins hold no pin named "${source.name}"`}`,
32
+ );
33
+ }
34
+ if (FIRST_PARTY_LAYOUT_PREFIX.test(pin.repository)) {
35
+ throw new Error(`third-party source "${pin.name}" pins repository "${pin.repository}", which is under the first-party layout (repos/ or submodules/)`);
36
+ }
37
+ if (repositoryPins.has(pin.repository)) {
38
+ throw new Error(`third-party source "${pin.name}" repository "${pin.repository}" shadows a repository pin the runtime image source index already commits`);
39
+ }
40
+ const shadowed = sources.find((candidate) => candidate.kind === 'git' && (candidate.name === pin.repository || candidate.name === pin.name));
41
+ if (shadowed !== undefined) {
42
+ throw new Error(`third-party source "${pin.name}" (${pin.repository}) collides with the git source "${shadowed.name}" of the same edition`);
43
+ }
44
+ const subjects = imageLock.subjects.filter((subject) => subject.repo === pin.repository);
45
+ if (subjects.length === 0) {continue;}
46
+ const wrong = subjects.filter((subject) => subject.sourceCommit !== pin.sourceCommit);
47
+ if (wrong.length > 0) {
48
+ throw new Error(
49
+ `third-party source "${pin.name}" is pinned at ${pin.sourceCommit}, but images.lock.json builds ${wrong.map((subject) => `${subject.kind}:${subject.id}`).join(', ')} at ${[...new Set(wrong.map((subject) => subject.sourceCommit))].join(', ')}`,
50
+ );
51
+ }
52
+ derived.set(pin.repository, { repo: pin.repository, repositoryUrl: pin.repositoryUrl, sourceCommit: pin.sourceCommit });
53
+ }
54
+ return derived;
55
+ }
56
+
57
+ /** The pins behind `thirdPartySourceRepositoryPins`, by repository. */
58
+ export const thirdPartySourcesByRepository = (derived, thirdPartyPins) =>
59
+ new Map([...derived.keys()].map((repo) => [repo, thirdPartyPins.byRepository(repo)]));
60
+
61
+ /**
62
+ * The ONE trust-policy row of `rows` (the `channel` image-source rows whose imageRepositories list `imageRef`): none, or
63
+ * several, is refused. The pin writer, `--check-third-party` and resolve-time admission hold a third-party image to it.
64
+ */
65
+ export function soleImageSignerRow({ rows, imageRef, channel, label }) {
66
+ if (rows.length !== 1) {
67
+ throw new Error(
68
+ `${label}: ${rows.length} ${channel} trust policy rows list ${imageRef} in their imageRepositories for image-source evidence; exactly one is required`,
69
+ );
70
+ }
71
+ return rows[0];
72
+ }
73
+
74
+ /**
75
+ * What a third-party subject must satisfy beyond a first-party one, after its evidence has verified:
76
+ * * [d9-1] the signing row's workflow repository, the subject's `repo` and the path of the predicate's
77
+ * `source.repositoryUrl` are one repository;
78
+ * * the subject is the pinned image (reference, content hash, sealed digest when sealed);
79
+ * * `imageRows` (the rows of the verification channel listing the image) hold exactly one row, the one the pin names
80
+ * for that image (`policyId`), and the evidence verified under it, signing the pinned `producerWorkflow`: another
81
+ * row listing the image, of the same repository or not, is refused. The bundle is held to
82
+ * `bundle.signerPolicyId` by the bundle signer selection (`locked-bundle-signer.mjs`);
83
+ * * (d) the leaf certificate states the pinned repository id, owner id and the runner environment the pin declares
84
+ * (`assertCertificateExtensions`: github-hosted unless the pin declares self-hosted, which also holds the build URIs).
85
+ */
86
+ export function assertThirdPartySubjectAdmission({ subject, source, statement, identityPolicy, imageRows, channel, bundle }) {
87
+ const label = `third-party subject "${subject.kind}:${subject.id}" (source "${source.name}")`;
88
+ const claimedRepository = repositoryPathOf(statement.predicate.source.repositoryUrl);
89
+ const signerRepository = identityPolicy.githubWorkflow.repository;
90
+ if (signerRepository !== subject.repo || claimedRepository !== subject.repo) {
91
+ throw new Error(
92
+ `${label}: the signing row's workflow repository (${signerRepository}), the subject repo (${subject.repo}) and the predicate source.repositoryUrl repository (${claimedRepository}) must be one repository`,
93
+ );
94
+ }
95
+ const image = source.images.find((candidate) => candidate.subjectKind === subject.kind && candidate.subjectId === subject.id);
96
+ if (image === undefined) {
97
+ throw new Error(`${label}: the pin pins no image for it`);
98
+ }
99
+ const sole = soleImageSignerRow({ rows: imageRows, imageRef: subject.imageRef, channel, label });
100
+ if (sole.policyId !== image.policyId) {
101
+ throw new Error(`${label}: the one ${channel} row listing ${subject.imageRef} is "${sole.policyId}", but the pin names row "${image.policyId}"`);
102
+ }
103
+ if (identityPolicy.policyId !== image.policyId || identityPolicy.githubWorkflow.path !== image.producerWorkflow) {
104
+ throw new Error(
105
+ `${label}: its evidence verified under row "${identityPolicy.policyId}" (workflow ${identityPolicy.githubWorkflow.path}), but the pin holds it to row "${image.policyId}" (workflow ${image.producerWorkflow})`,
106
+ );
107
+ }
108
+ if (subject.imageRef !== image.imageRef) {
109
+ throw new Error(`${label}: images.lock.json names ${subject.imageRef}, but the pin pins ${image.imageRef}`);
110
+ }
111
+ if (subject.contentHash !== image.sourceTreeHash) {
112
+ throw new Error(`${label}: images.lock.json has contentHash ${subject.contentHash}, but the pin records sourceTreeHash ${image.sourceTreeHash}`);
113
+ }
114
+ if (subject.imageDigest !== undefined && subject.imageDigest !== image.manifestDigest) {
115
+ throw new Error(`${label}: images.lock.json seals ${subject.imageDigest}, but the pin records manifestDigest ${image.manifestDigest}`);
116
+ }
117
+ assertCertificateExtensions({ bundle, source, signerIdentity: identityPolicy.certificateIdentity, label });
118
+ }