@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.
- package/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- 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
|
+
}
|