@xemahq/edition-tooling 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,357 @@
1
+ // authorization-base — the edition's composed AUTHORIZATION BASE (plan
2
+ // `biome-declared-authorization.md` §2.3 steps 3–7, §11 step 4).
3
+ //
4
+ // An edition's base is the platform modules the KERNEL ships (`xema_core`,
5
+ // `xema_capability`, `xema_sharing`) plus every authorization module the
6
+ // edition's biomes ship. This file:
7
+ //
8
+ // 1. reads each biome's module FROM ITS PINNED SOURCE into the biome index
9
+ // (`readBiomeAuthorizationModuleAt`), so the sealer composes the exact
10
+ // bytes the biome bundle signs;
11
+ // 2. composes with the KERNEL composer — `composeAuthorizationBase` from
12
+ // `@xemahq/contracts/authorization-module/server`, loaded from the
13
+ // edition's pinned, built kernel — never a local re-implementation;
14
+ // 3. runs every composed `.fga.yaml` (platform and biome) with the standard
15
+ // `fga model test`, from the composition root the composer describes —
16
+ // one failure refuses the edition;
17
+ // 4. seals `authorization.lock.json` beside `distribution.lock.json`: the
18
+ // `modelHash` authorization-api adopts, the model, the platform
19
+ // generation it was composed for, and the module provenance.
20
+ //
21
+ // The platform generation is the pinned kernel's own declared
22
+ // `PLATFORM_GENERATION` integer (kernel `platform-generation` subpath), read
23
+ // from the built kernel and never written down here.
24
+ //
25
+ // A pinned kernel that does not ship the composer has no authorization plane
26
+ // to seal: the resolver retires any lock, and dist-check refuses a committed
27
+ // lock that kernel cannot reproduce. Once the pin carries the composer, every
28
+ // edition seals — the platform modules always compose.
29
+
30
+ import { execFileSync } from 'node:child_process';
31
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
32
+ import { tmpdir } from 'node:os';
33
+ import { dirname, join } from 'node:path';
34
+
35
+ import { RefusalError } from './collected-refusals.mjs';
36
+ import { assertKernelEditionProtocol } from './edition-protocol.mjs';
37
+
38
+ /** Where a biome keeps its module, relative to its manifest directory (plan §2.1). */
39
+ export const AUTHORIZATION_MODULE_DIRECTORY = 'authorization';
40
+ export const AUTHORIZATION_ENVELOPE_FILENAME = 'envelope.json';
41
+ export const AUTHORIZATION_LOCK_FILENAME = 'authorization.lock.json';
42
+
43
+ /**
44
+ * The OpenFGA CLI the sealer tests with, EXACTLY. 0.8.1 embeds
45
+ * openfga/openfga v1.21.0 (github) (its go.mod), the server version every
46
+ * installation runs (xema-infra-services chart 0.3.15) — so a sealed test ran
47
+ * against the same engine that will answer in production.
48
+ */
49
+ export const FGA_CLI_VERSION = '0.8.1';
50
+ /** The environment variable naming the `fga` binary the sealer runs. */
51
+ export const FGA_CLI_ENV = 'XEMA_FGA_CLI';
52
+
53
+ /**
54
+ * Read a biome's authorization module at a commit, or `null` when the biome
55
+ * ships none. The shape is the kernel's
56
+ * `AuthorizationModuleContributionManifest`: `{ envelope, moduleFiles,
57
+ * testFile }` — the envelope plus the CONTENTS of every file it names.
58
+ *
59
+ * `readText(repoRelativePath)` returns a file's text at the pinned commit, or
60
+ * `null` when it does not exist there. A present envelope naming an absent
61
+ * file is a refusal, never a partial module. The envelope itself is validated
62
+ * by the kernel composer, not here.
63
+ */
64
+ export function readBiomeAuthorizationModuleAt(manifestDirectory, readText, rel) {
65
+ const base = `${manifestDirectory}/${AUTHORIZATION_MODULE_DIRECTORY}`;
66
+ const envelopeText = readText(`${base}/${AUTHORIZATION_ENVELOPE_FILENAME}`);
67
+ if (envelopeText === null) {return null;}
68
+ let envelope;
69
+ try {
70
+ envelope = JSON.parse(envelopeText);
71
+ } catch (err) {
72
+ throw new RefusalError(`${rel}: ${base}/${AUTHORIZATION_ENVELOPE_FILENAME} is not JSON: ${err.message}`);
73
+ }
74
+ const files = Array.isArray(envelope?.files) ? envelope.files : null;
75
+ if (!files || files.length === 0 || typeof envelope.tests !== 'string') {
76
+ throw new RefusalError(
77
+ `${rel}: ${base}/${AUTHORIZATION_ENVELOPE_FILENAME} must name its module files ("files") ` +
78
+ 'and its REQUIRED OpenFGA test file ("tests").',
79
+ );
80
+ }
81
+ const read = (name) => {
82
+ const contents = readText(`${base}/${name}`);
83
+ if (contents === null) {
84
+ throw new RefusalError(`${rel}: the authorization envelope names ${base}/${name}, which does not exist at the pinned commit.`);
85
+ }
86
+ return { name, contents };
87
+ };
88
+ return {
89
+ envelope,
90
+ moduleFiles: [...files].sort().map(read),
91
+ testFile: read(envelope.tests),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * The composer's `AuthorizationModuleInput[]` for an edition: one per selected
97
+ * biome that ships a module, sorted by id. Built with the KERNEL's own two
98
+ * helpers, never by hand:
99
+ *
100
+ * manifest = stampAuthorizationModuleContribution(source, signedBiome)
101
+ * input = authorizationModuleInputFromContribution({ owner, manifest })
102
+ *
103
+ * The sealer reads modules from the biome index rather than through
104
+ * contribution-sync, so it is the sealer that stamps: `signedBiome` (tier,
105
+ * declared biome dependencies, service names) was derived from the pinned
106
+ * `xema-biome.json` when the index was built, and the owner is the lock's
107
+ * signed id and version. A pinned kernel without the composer cannot stamp,
108
+ * so an edition carrying a module under such a pin is refused.
109
+ */
110
+ export function editionAuthorizationModuleInputs(lockBiomes, indexById, kernel) {
111
+ const inputs = [];
112
+ for (const { id, version } of lockBiomes) {
113
+ const entry = indexById.get(id);
114
+ if (!entry) {throw new RefusalError(`edition biome "${id}" is missing from the biome index`);}
115
+ if (!entry.authorizationModule) {continue;}
116
+ if (kernel === null) {
117
+ throw new RefusalError(
118
+ `edition biome "${id}" ships an authorization module, but the pinned kernel ships no composer to stamp or compose it.`,
119
+ );
120
+ }
121
+ const { source, signedBiome } = entry.authorizationModule;
122
+ const manifest = kernel.stampAuthorizationModuleContribution(source, signedBiome);
123
+ inputs.push(kernel.authorizationModuleInputFromContribution({ owner: { id, version }, manifest }));
124
+ }
125
+ return inputs.sort((x, y) => (x.signedBiomeId < y.signedBiomeId ? -1 : x.signedBiomeId > y.signedBiomeId ? 1 : 0));
126
+ }
127
+
128
+ /**
129
+ * The pinned kernel's authorization composer, or `null` when that kernel does
130
+ * not ship one (its built `authorization-module/server` subpath is absent).
131
+ * An ABSENT subpath is a fact about the pin; a PRESENT one that fails to load
132
+ * or lacks the export is an error.
133
+ *
134
+ * The platform generation is the kernel's declared `PLATFORM_GENERATION`
135
+ * integer, validated by the kernel's own `PlatformGenerationSchema`. It is NOT
136
+ * the package version: an additive kernel release does not move it.
137
+ */
138
+ export async function loadKernelAuthorizationComposer({ kernelPackageDir }) {
139
+ assertKernelEditionProtocol(kernelPackageDir);
140
+ const serverDist = join(kernelPackageDir, 'dist', 'authorization-module', 'server', 'index.js');
141
+ if (!existsSync(serverDist)) {return null;}
142
+ let server;
143
+ let contribution;
144
+ let generation;
145
+ let distribution;
146
+ try {
147
+ server = await import(`file://${serverDist}`);
148
+ contribution = await import(`file://${join(kernelPackageDir, 'dist', 'authorization-module', 'index.js')}`);
149
+ generation = await import(`file://${join(kernelPackageDir, 'dist', 'platform-generation', 'index.js')}`);
150
+ distribution = await import(`file://${join(kernelPackageDir, 'dist', 'distribution', 'index.js')}`);
151
+ } catch (err) {
152
+ throw new Error(`Cannot load the pinned kernel's authorization composer from ${serverDist}: ${err.message}`);
153
+ }
154
+ for (const [mod, name] of [
155
+ [server, 'composeAuthorizationBase'],
156
+ [server, 'authorizationModuleInputFromContribution'],
157
+ [contribution, 'stampAuthorizationModuleContribution'],
158
+ [distribution, 'parseAuthorizationLockReadView'],
159
+ ]) {
160
+ if (typeof mod[name] !== 'function') {
161
+ throw new Error(`the pinned kernel's authorization-module subpaths do not export ${name}.`);
162
+ }
163
+ }
164
+ const parsed = generation.PlatformGenerationSchema?.safeParse?.(generation.PLATFORM_GENERATION);
165
+ if (!parsed?.success) {
166
+ throw new Error(
167
+ "the pinned kernel's platform-generation subpath does not export a valid PLATFORM_GENERATION " +
168
+ `(a positive integer validated by PlatformGenerationSchema); got ${JSON.stringify(generation.PLATFORM_GENERATION)}.`,
169
+ );
170
+ }
171
+ return {
172
+ composeAuthorizationBase: server.composeAuthorizationBase,
173
+ authorizationModuleInputFromContribution: server.authorizationModuleInputFromContribution,
174
+ stampAuthorizationModuleContribution: contribution.stampAuthorizationModuleContribution,
175
+ parseAuthorizationLockReadView: distribution.parseAuthorizationLockReadView,
176
+ authorizationLockSchemaVersion: distribution.AUTHORIZATION_LOCK_SCHEMA_VERSION,
177
+ currentPlatformGeneration: parsed.data,
178
+ kernelPackageDir,
179
+ };
180
+ }
181
+
182
+ /** The `adopted` input the composer diffs against: the committed lock, if any. */
183
+ export function adoptedFromLock(lock) {
184
+ if (!lock) {return undefined;}
185
+ return {
186
+ model: lock.model,
187
+ modules: Object.fromEntries(
188
+ lock.modules.map((m) => [m.namespace, { contentHash: m.contentHash }]),
189
+ ),
190
+ };
191
+ }
192
+
193
+ /** Compose an edition's base; a refusal names the edition and every violation. */
194
+ export function composeEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, biomeOrigins }) {
195
+ try {
196
+ return kernel.composeAuthorizationBase({
197
+ modules: inputs,
198
+ biomeOrigins,
199
+ currentPlatformGeneration: kernel.currentPlatformGeneration,
200
+ ...(adopted ? { adopted } : {}),
201
+ });
202
+ } catch (err) {
203
+ if (Array.isArray(err?.violations)) {
204
+ throw new RefusalError(`${distributionId}: the authorization base was REFUSED by the kernel composer.\n${err.message}`);
205
+ }
206
+ throw err;
207
+ }
208
+ }
209
+
210
+ /** The lock body for a composed base. Deterministic: no timestamp. It is read through the kernel's `parseAuthorizationLockReadView` before it is returned. */
211
+ export function authorizationLockOf(distributionId, kernel, composed) {
212
+ const lock = {
213
+ schemaVersion: kernel.authorizationLockSchemaVersion,
214
+ distributionId,
215
+ platformGeneration: kernel.currentPlatformGeneration,
216
+ modelHash: composed.modelHash,
217
+ modelBytes: composed.modelBytes,
218
+ testedWith: `openfga/cli v${FGA_CLI_VERSION}`,
219
+ modules: composed.modules.map((m) => ({
220
+ namespace: m.namespace,
221
+ contentHash: m.contentHash,
222
+ signedBiomeVersion: m.signedBiomeVersion,
223
+ })),
224
+ parentImplementers: [...composed.parentImplementers],
225
+ model: composed.model,
226
+ };
227
+ kernel.parseAuthorizationLockReadView(lock);
228
+ return lock;
229
+ }
230
+
231
+ /** Resolve and verify the pinned `fga` CLI. */
232
+ export function resolveFgaCli(env = process.env) {
233
+ const bin = env[FGA_CLI_ENV];
234
+ if (!bin) {
235
+ throw new Error(
236
+ `sealing an authorization base runs \`fga model test\`, and ${FGA_CLI_ENV} is unset. ` +
237
+ `Point it at the openfga/cli v${FGA_CLI_VERSION} binary.`,
238
+ );
239
+ }
240
+ const out = execFileSync(bin, ['version'], { encoding: 'utf8' });
241
+ const match = /v`?(\d+\.\d+\.\d+)`?/.exec(out);
242
+ if (!match || match[1] !== FGA_CLI_VERSION) {
243
+ throw new Error(`${FGA_CLI_ENV}=${bin} reports "${out.trim()}"; the sealer requires exactly v${FGA_CLI_VERSION}.`);
244
+ }
245
+ return bin;
246
+ }
247
+
248
+ /**
249
+ * Run every composed `.fga.yaml` with the real CLI, from the composition root:
250
+ * the composer's `files` (the filled templates, the biome files and `fga.mod`)
251
+ * and `testFiles` are written at their composed names, and each test runs
252
+ * with that root as its working directory.
253
+ */
254
+ export function fgaComposedModelTestRunner(bin) {
255
+ return (composed) => {
256
+ const root = mkdtempSync(join(tmpdir(), 'xema-authz-seal-'));
257
+ try {
258
+ for (const f of [...composed.files, ...composed.testFiles]) {
259
+ mkdirSync(dirname(join(root, f.name)), { recursive: true });
260
+ writeFileSync(join(root, f.name), f.contents);
261
+ }
262
+ if (composed.testFiles.length === 0) {
263
+ throw new RefusalError('the composed base carries no test file — every module ships one, the platform modules included.');
264
+ }
265
+ for (const t of composed.testFiles) {
266
+ try {
267
+ execFileSync(bin, ['model', 'test', '--tests', t.name], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
268
+ } catch (err) {
269
+ throw new RefusalError(
270
+ `${t.owner}: ${t.name} FAILED against the composed edition base:\n` +
271
+ `${String(err.stdout ?? '').trim()}\n${String(err.stderr ?? '').trim()}`,
272
+ );
273
+ }
274
+ }
275
+ } finally {
276
+ rmSync(root, { recursive: true, force: true });
277
+ }
278
+ };
279
+ }
280
+
281
+ /** Compose, test, describe. `runTests` is the `fga model test` runner. */
282
+ export function sealEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, biomeOrigins, runTests }) {
283
+ const composed = composeEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, biomeOrigins });
284
+ runTests(composed);
285
+ return authorizationLockOf(distributionId, kernel, composed);
286
+ }
287
+
288
+ /** Outcome of the committed-vs-recomposed authorization lock comparison. */
289
+ export const AuthorizationLockCheck = Object.freeze({
290
+ GREEN: 'green',
291
+ MISSING: 'missing',
292
+ STALE: 'stale',
293
+ NOT_SEALABLE: 'not-sealable',
294
+ });
295
+
296
+ /**
297
+ * Is the committed `authorization.lock.json` what this edition composes NOW?
298
+ * Recomposes with the committed lock as `adopted` (so an undeclared
299
+ * incompatible change is refused exactly as the sealer would refuse it; a
300
+ * module's identity is its content hash, so changed content is a new identity
301
+ * and neither a reused version nor a downgrade exists) and compares the whole lock. The
302
+ * tests are not re-run: an equal `modelHash` is the model the sealer tested.
303
+ */
304
+ export function checkAuthorizationLockCurrent({ distributionId, committed, inputs, kernel, biomeOrigins, serialize }) {
305
+ if (kernel === null) {
306
+ return committed === null ? AuthorizationLockCheck.GREEN : AuthorizationLockCheck.NOT_SEALABLE;
307
+ }
308
+ if (committed === null) {return AuthorizationLockCheck.MISSING;}
309
+ const composed = composeEditionAuthorizationBase({
310
+ distributionId,
311
+ inputs,
312
+ kernel,
313
+ biomeOrigins,
314
+ adopted: adoptedFromLock(committed),
315
+ });
316
+ return serialize(authorizationLockOf(distributionId, kernel, composed)) === serialize(committed)
317
+ ? AuthorizationLockCheck.GREEN
318
+ : AuthorizationLockCheck.STALE;
319
+ }
320
+
321
+ /**
322
+ * dist-check's authorization-currency verdict for ONE edition: the committed
323
+ * `authorization.lock.json` against a fresh composition over the edition's
324
+ * lock biomes, looked up in the index that lock was RESOLVED FROM (platform
325
+ * entries plus the edition's source-walked ones). Returns an
326
+ * `AuthorizationLockCheck` member; throws on a refusal.
327
+ */
328
+ export function checkEditionAuthorizationLock({ distributionId, editionDir, lockBiomes, biomeOrigins, index, kernel, serialize }) {
329
+ if (!Array.isArray(index)) {
330
+ throw new Error(`${distributionId}: the authorization check needs the index the lock was resolved from.`);
331
+ }
332
+ return checkAuthorizationLockCurrent({
333
+ distributionId,
334
+ committed: readAuthorizationLock(editionDir),
335
+ inputs: editionAuthorizationModuleInputs(lockBiomes, new Map(index.map((e) => [e.id, e])), kernel),
336
+ kernel,
337
+ biomeOrigins,
338
+ serialize,
339
+ });
340
+ }
341
+
342
+ /** Read an edition's committed authorization lock, or `null`. */
343
+ export function readAuthorizationLock(editionDir) {
344
+ const path = join(editionDir, AUTHORIZATION_LOCK_FILENAME);
345
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
346
+ }
347
+
348
+ /** Write (or retire) an edition's authorization lock. */
349
+ export function writeAuthorizationLock(editionDir, lock, serialize) {
350
+ const path = join(editionDir, AUTHORIZATION_LOCK_FILENAME);
351
+ if (lock === null) {
352
+ if (existsSync(path)) {rmSync(path);}
353
+ return null;
354
+ }
355
+ writeFileSync(path, serialize(lock));
356
+ return path;
357
+ }