@fabricorg/fabric-gen 0.3.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,136 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e9a2f5e: Add `fabric-gen promote-experience`, which turns a validated release into a signed one from a
8
+ declared configuration: the assembly it was validated against, the release, and the usage contracts
9
+ routes are compiled from.
10
+
11
+ The signing key is a `--key` argument rather than a configuration field, and a configuration that
12
+ carries one is refused, because a configuration file is the kind of thing that ends up committed.
13
+
14
+ ### Patch Changes
15
+
16
+ - a598fb9: Close the remaining review findings.
17
+
18
+ **One capability-reference grammar.** `sdui-release` accepted any name after the namespace while
19
+ `sdui-composer` rejected `/`, so a view named `orders/reports/daily` produced a release that validated
20
+ and then died at promotion with nothing wrong upstream to point at. Both now share
21
+ `parseCapabilityRef`, tightened to lowercase path segments matching what the platform permits for a
22
+ view name. Two views in one contract that reduce to the same reference are reported as
23
+ `ambiguous_capability_reference` at validation rather than surfacing as an ambiguous route later.
24
+
25
+ **A bounded replay window.** A client-supplied `capturedAt` had no lower bound. Deferring it to the
26
+ Host looked safe and was not: the Host checks staleness only when the capture-authority adapter
27
+ supplies an `expiresAt`, and skips the execution-time expiry check entirely for actions governed at
28
+ capture. A gateway that accepts replay must now declare `maxOfflineReplayAgeMs`, and the instant must
29
+ be RFC 3339 with an explicit offset.
30
+
31
+ **One token-set grammar.** `document.tokenSet` accepted names and versions that `supportedTokenSets`
32
+ rejected, while validation treats the two fields as interchangeable.
33
+
34
+ **Port requirements narrow as they are checked.** A requirement for a standard _and_ a version could
35
+ be satisfied by two different adapters covering one constraint each. `requirement.standard.version`
36
+ was declared and never read.
37
+
38
+ - 0d1f6f6: Close what a third independent review found, including two defects introduced by the previous fix.
39
+
40
+ **Offline replay counted refusals as successes.** `PlatformHost` reports a refused capture by
41
+ _returning_ `reconciliation_required` with the reason, not by throwing, so the queue took its success
42
+ branch, deleted the capture and reported it applied. The reconciliation path was unreachable. Replay
43
+ now reads the returned outcome. The already-applied branch is removed entirely: a repeated key
44
+ returns the original invocation without complaint, and an idempotency _conflict_ means the key was
45
+ reused for a materially different command, so that capture was never applied and is kept rather than
46
+ discarded.
47
+
48
+ **`deriveAuthorizationGrants` defaulted open and reached the signed artifact.** Requirements are
49
+ optional metadata, so a capability declaring none granted everything to an actor holding nothing.
50
+ Grants are also carried onto a release unexamined and promotion signs a route for each, so a surplus
51
+ grant became a signed route to a capability the screen never renders. Undeclared requirements now
52
+ withhold by default, and `document` scopes the result to references the screen actually binds.
53
+
54
+ Several conformance checks could not fail: a search suite passed an adapter returning nothing, a
55
+ payments suite passed a double-charger that omitted a provider reference, an identity suite certified
56
+ timestamps the scope check then rejected on every request, an MFE probe could be refused for an
57
+ unrelated reason, and a channel check accepted any exception as a refusal. Each now proves what it
58
+ claims. Promotion configuration rejects anything reading like a key rather than two spellings,
59
+ rejects unknown fields, and constrains the application name that decides the default output path.
60
+
61
+ - Updated dependencies [8ce5e06]
62
+ - Updated dependencies [a598fb9]
63
+ - Updated dependencies [8f75ac9]
64
+ - Updated dependencies [9a5b18a]
65
+ - Updated dependencies [9b2e55b]
66
+ - Updated dependencies [0d1f6f6]
67
+ - Updated dependencies [274d948]
68
+ - @fabricorg/sdui-release@0.5.0
69
+ - @fabricorg/sdui-composer@0.2.0
70
+ - @fabricorg/assembly@0.4.0
71
+ - @fabricorg/gen-conformance@1.0.0
72
+ - @fabricorg/gen-graphql@4.0.0
73
+
74
+ ## 0.5.0
75
+
76
+ ### Minor Changes
77
+
78
+ - Add `fabric-gen init experience` for wiring the supported experience tier into an
79
+ existing application without generating a competing shell.
80
+
81
+ ### Patch Changes
82
+
83
+ - Updated dependencies
84
+ - @fabricorg/sdui-release@0.4.0
85
+
86
+ ## 0.4.1
87
+
88
+ ### Patch Changes
89
+
90
+ - Updated dependencies
91
+ - Updated dependencies [89982ba]
92
+ - Updated dependencies [6f5471b]
93
+ - Updated dependencies [89982ba]
94
+ - Updated dependencies [89982ba]
95
+ - @fabricorg/compiler@2.2.0
96
+ - @fabricorg/gen-capability@0.5.0
97
+ - @fabricorg/gen-conformance@0.3.0
98
+ - @fabricorg/gen-graphql@3.1.0
99
+ - @fabricorg/sdui-release@0.3.0
100
+ - @fabricorg/assembly@0.3.0
101
+
102
+ ## 0.4.0
103
+
104
+ ### Minor Changes
105
+
106
+ - bd67ad6: Add non-destructive capability initialization and fail-closed application
107
+ composition checks for approved lockfiles, exact runtime artifacts, and
108
+ promoted experience releases.
109
+
110
+ ### Patch Changes
111
+
112
+ - Updated dependencies [62dd1ca]
113
+ - Updated dependencies [4e585a4]
114
+ - Updated dependencies [b932767]
115
+ - Updated dependencies [1ba3a9e]
116
+ - Updated dependencies [8e9a571]
117
+ - Updated dependencies [e6aede7]
118
+ - @fabricorg/assembly@0.2.0
119
+ - @fabricorg/gen-capability@0.4.1
120
+ - @fabricorg/sdui-release@0.2.0
121
+ - @fabricorg/compiler@2.1.1
122
+ - @fabricorg/gen-conformance@0.2.1
123
+ - @fabricorg/gen-graphql@3.0.1
124
+ - @fabricorg/gen-sdui@0.5.1
125
+
126
+ ### Additional release notes
127
+
128
+ ### Minor Changes
129
+
130
+ - Add `init capability` for non-destructive compiler-valid scaffolding and
131
+ `check-application` for exact runtime capability, assembly lockfile, and
132
+ promoted experience release verification.
133
+
3
134
  ## 0.3.2
4
135
 
5
136
  ### Patch Changes
@@ -8,7 +139,7 @@
8
139
  - Updated dependencies [c504d0a]
9
140
  - @fabricorg/compiler@2.1.0
10
141
  - @fabricorg/gen-graphql@3.0.0
11
- - @fabricorg/gen-conformance@1.0.0
142
+ - @fabricorg/gen-conformance@0.2.0
12
143
  - @fabricorg/gen-capability@0.4.0
13
144
 
14
145
  ## 0.3.1
package/README.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # `fabric-gen`
2
2
 
3
- Thin CLI over independently versioned Fabric generator targets.
3
+ Scaffold capability source, run independently versioned generators, and verify
4
+ an application composition before deployment.
5
+
6
+ Create a compiler-valid capability without copying a repository template:
7
+
8
+ ```bash
9
+ fabric-gen init capability case-review
10
+ fabric-gen init experience portfolio --directory apps/saas/modules/portfolio
11
+ fabric-gen init capability case-review --directory packages/case-review
12
+ ```
13
+
14
+ `init experience` adds runtime, gateway, component-pack, and application-check
15
+ integration files to an existing Supastarter-derived application. It never creates or
16
+ replaces the application shell.
17
+
18
+ Initialization refuses to overwrite either `README.md` or
19
+ `fabric.manifest.json`.
4
20
 
5
21
  ```bash
6
22
  fabric-gen --input fabric-manifest.json --out generated
@@ -9,3 +25,27 @@ fabric-gen --input fabric-manifest.json --out generated --check
9
25
 
10
26
  `--check` performs no writes and fails for changed or stale artifacts. Generated files contain a
11
27
  manifest digest and generator version, never timestamps.
28
+
29
+ Check the exact artifacts an application will load:
30
+
31
+ ```bash
32
+ fabric-gen check-application --config fabric.application.json
33
+ ```
34
+
35
+ `fabric.application.json` uses paths relative to itself:
36
+
37
+ ```json
38
+ {
39
+ "formatVersion": 1,
40
+ "assembly": "./fabric.assembly.lock.json",
41
+ "runtimeCapabilities": "./fabric.runtime.json",
42
+ "releases": ["./releases/web.json"]
43
+ }
44
+ ```
45
+
46
+ The runtime file is an array of `{ "namespace", "version", "manifestDigest" }`
47
+ objects derived from the modules actually registered by the application. The
48
+ check verifies lockfile integrity, rejects missing, extra, duplicate, or
49
+ mismatched runtime capabilities, verifies each promoted experience release,
50
+ and requires every release to name the same assembly digest. It does not
51
+ inspect caller assertions in place of runtime artifact metadata.
@@ -0,0 +1,413 @@
1
+ // index.ts
2
+ import { access, mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
3
+ import { createPrivateKey } from "crypto";
4
+ import path from "path";
5
+ import { compileManifestBundle, runGenerator } from "@fabricorg/compiler";
6
+ import { capabilityGenerator } from "@fabricorg/gen-capability";
7
+ import { docsGenerator } from "@fabricorg/gen-docs";
8
+ import { eventsGenerator } from "@fabricorg/gen-events";
9
+ import { typesGenerator } from "@fabricorg/gen-types";
10
+ import { prismaGenerator } from "@fabricorg/gen-prisma";
11
+ import { graphqlGenerator } from "@fabricorg/gen-graphql";
12
+ import { sduiGenerator } from "@fabricorg/gen-sdui";
13
+ import { createEs256ReleaseSigner, promoteExperienceRelease } from "@fabricorg/sdui-composer";
14
+ import { conformanceGenerator } from "@fabricorg/gen-conformance";
15
+ import {
16
+ assertAssemblyLockfile,
17
+ assertAssemblyRuntimeCompatible
18
+ } from "@fabricorg/assembly";
19
+ import {
20
+ assertSduiReleaseIntegrity
21
+ } from "@fabricorg/sdui-release";
22
+ var GENERATOR_TARGETS = {
23
+ types: typesGenerator,
24
+ events: eventsGenerator,
25
+ capability: capabilityGenerator,
26
+ docs: docsGenerator,
27
+ prisma: prismaGenerator,
28
+ graphql: graphqlGenerator,
29
+ sdui: sduiGenerator,
30
+ conformance: conformanceGenerator
31
+ };
32
+ var compareCanonicalStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
33
+ var usage = [
34
+ "Usage:",
35
+ " fabric-gen init capability <namespace> [--directory <path>]",
36
+ " fabric-gen init experience <application> [--directory <path>]",
37
+ " fabric-gen check-application [--config <path>]",
38
+ " fabric-gen promote-experience --config <path> --key <pem> [--out <path>]",
39
+ " fabric-gen --input <manifest.json> --out <directory> [--targets <list>] [--check]"
40
+ ].join("\n");
41
+ var FabricGenUsageError = class extends Error {
42
+ name = "FabricGenUsageError";
43
+ };
44
+ function fabricGenCliExitCode(error) {
45
+ return error instanceof FabricGenUsageError ? 2 : 1;
46
+ }
47
+ function flagValue(args, flag) {
48
+ const index = args.indexOf(flag);
49
+ if (index < 0) return void 0;
50
+ const value = args[index + 1];
51
+ if (!value || value.startsWith("--")) throw new FabricGenUsageError(usage);
52
+ return value;
53
+ }
54
+ async function runFabricGenCommand(args) {
55
+ if (args[0] === "init") {
56
+ if (!args[2] || args[2].startsWith("--")) {
57
+ throw new FabricGenUsageError(usage);
58
+ }
59
+ const name = args[2];
60
+ const directory = flagValue(args, "--directory") ?? name;
61
+ if (args[1] === "capability") {
62
+ const result2 = await initializeCapability({ directory, namespace: name });
63
+ return `Initialized capability "${name}" with ${result2.created.length} files.`;
64
+ }
65
+ if (args[1] === "experience") {
66
+ const result2 = await initializeExperience({ directory, application: name });
67
+ return `Initialized experience integration "${name}" with ${result2.created.length} files.`;
68
+ }
69
+ throw new FabricGenUsageError(usage);
70
+ }
71
+ if (args[0] === "promote-experience") {
72
+ const configPath = flagValue(args, "--config");
73
+ const privateKeyPath = flagValue(args, "--key");
74
+ if (!configPath) throw new FabricGenUsageError("promote-experience requires --config.");
75
+ if (!privateKeyPath) throw new FabricGenUsageError("promote-experience requires --key.");
76
+ const outputPath = flagValue(args, "--out");
77
+ const result2 = await promoteExperience({
78
+ configPath,
79
+ privateKeyPath,
80
+ ...outputPath ? { outputPath } : {}
81
+ });
82
+ return [
83
+ `Promoted ${result2.application} release ${result2.releaseDigest.slice(0, 12)} against assembly ${result2.assemblyDigest.slice(0, 12)}.`,
84
+ `Routes: ${result2.routes.views} views, ${result2.routes.intents} intents.`,
85
+ `Wrote ${result2.outputPath}`
86
+ ].join("\n");
87
+ }
88
+ if (args[0] === "check-application") {
89
+ const configPath = flagValue(args, "--config") ?? "fabric.application.json";
90
+ const result2 = await checkApplication({ configPath });
91
+ return `Checked "${result2.application}": ${result2.capabilities} capabilities, ${result2.releases} releases.`;
92
+ }
93
+ const inputPath = flagValue(args, "--input");
94
+ const outputDirectory = flagValue(args, "--out");
95
+ if (!inputPath || !outputDirectory) throw new FabricGenUsageError(usage);
96
+ const result = await generateFromManifest({
97
+ inputPath,
98
+ outputDirectory,
99
+ targets: (flagValue(args, "--targets") ?? "types,events,capability,docs").split(",").filter(Boolean),
100
+ check: args.includes("--check")
101
+ });
102
+ return `${args.includes("--check") ? "Checked" : "Generated"} ${result.files.length} files.`;
103
+ }
104
+ async function initializeExperience(options) {
105
+ if (!/^[a-z][a-z0-9-]*$/.test(options.application)) {
106
+ throw new Error("Application name must use lowercase letters, numbers, and hyphens.");
107
+ }
108
+ const functionName = options.application.split("-").map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
109
+ const files = [
110
+ [
111
+ path.join(options.directory, "fabric.experience.ts"),
112
+ [
113
+ 'import { createExperienceServerRuntime } from "@fabricorg/experience-runtime/server";',
114
+ 'import { createExperienceGateway } from "@fabricorg/experience-gateway";',
115
+ "",
116
+ `export const create${functionName}ExperienceRuntime = (`,
117
+ " options: Parameters<typeof createExperienceServerRuntime>[0],",
118
+ ") => createExperienceServerRuntime(options);",
119
+ "",
120
+ `export const create${functionName}ExperienceGateway = (`,
121
+ " options: Parameters<typeof createExperienceGateway>[0],",
122
+ ") => createExperienceGateway(options);",
123
+ ""
124
+ ].join("\n")
125
+ ],
126
+ [
127
+ path.join(options.directory, "fabric.component-pack.ts"),
128
+ [
129
+ 'import { defineComponentPack } from "@fabricorg/component-pack";',
130
+ "",
131
+ `export const define${functionName}ComponentPack = (artifactDigest: string) =>`,
132
+ " defineComponentPack({",
133
+ ` name: "${options.application}",`,
134
+ ` namespace: "${options.application}",`,
135
+ ' version: "0.1.0",',
136
+ " artifactDigest,",
137
+ ' tokenContract: { name: "fabric-family", version: "1.0.0" },',
138
+ " components: {",
139
+ ' Text: { module: "./components/text.js", roles: ["content"], slots: [] },',
140
+ " },",
141
+ " });",
142
+ ""
143
+ ].join("\n")
144
+ ],
145
+ [
146
+ path.join(options.directory, "fabric.application.json"),
147
+ `${JSON.stringify({
148
+ formatVersion: 1,
149
+ application: options.application,
150
+ assembly: "./fabric.assembly.lock.json",
151
+ runtimeCapabilities: "./fabric.runtime.json",
152
+ releases: []
153
+ }, null, 2)}
154
+ `
155
+ ]
156
+ ];
157
+ await mkdir(options.directory, { recursive: true });
158
+ const created = [];
159
+ try {
160
+ for (const [file, content] of files) {
161
+ await writeFile(file, content, { encoding: "utf8", flag: "wx" });
162
+ created.push(file);
163
+ }
164
+ } catch (error) {
165
+ await Promise.all(created.map((file) => rm(file, { force: true })));
166
+ if (error.code === "EEXIST") {
167
+ throw new Error("Refusing to overwrite an existing experience integration file.");
168
+ }
169
+ throw error;
170
+ }
171
+ return { created };
172
+ }
173
+ async function readJson(file) {
174
+ return JSON.parse(await readFile(file, "utf8"));
175
+ }
176
+ function assertApplicationCheckConfig(value) {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
178
+ throw new Error("Application check configuration must be a JSON object.");
179
+ }
180
+ const config = value;
181
+ if (config.formatVersion !== 1) throw new Error("Application check configuration formatVersion must be 1.");
182
+ if (typeof config.assembly !== "string" || !config.assembly) {
183
+ throw new Error("Application check configuration assembly must be a file path.");
184
+ }
185
+ if (typeof config.runtimeCapabilities !== "string" || !config.runtimeCapabilities) {
186
+ throw new Error("Application check configuration runtimeCapabilities must be a file path.");
187
+ }
188
+ if (!Array.isArray(config.releases) || !config.releases.every((entry) => typeof entry === "string" && entry)) {
189
+ throw new Error("Application check configuration releases must be an array of file paths.");
190
+ }
191
+ }
192
+ async function checkApplication(options) {
193
+ const configValue = await readJson(options.configPath);
194
+ assertApplicationCheckConfig(configValue);
195
+ const base = path.dirname(options.configPath);
196
+ const resolve = (file) => path.resolve(base, file);
197
+ const assembly = await readJson(resolve(configValue.assembly));
198
+ assertAssemblyLockfile(assembly);
199
+ const runtimeCapabilities = await readJson(
200
+ resolve(configValue.runtimeCapabilities)
201
+ );
202
+ if (!Array.isArray(runtimeCapabilities)) {
203
+ throw new Error("Runtime capabilities must be a JSON array.");
204
+ }
205
+ assertAssemblyRuntimeCompatible(assembly, runtimeCapabilities);
206
+ for (const releasePath of configValue.releases) {
207
+ const release = await readJson(resolve(releasePath));
208
+ try {
209
+ assertSduiReleaseIntegrity(release);
210
+ } catch (error) {
211
+ throw new Error(
212
+ `Experience release "${releasePath}" is invalid: ${error instanceof Error ? error.message : String(error)}`
213
+ );
214
+ }
215
+ if (release.assemblyDigest !== assembly.assemblyDigest) {
216
+ throw new Error(
217
+ `Experience release "${release.document.name}@${release.document.version}" belongs to assembly "${release.assemblyDigest}", not "${assembly.assemblyDigest}".`
218
+ );
219
+ }
220
+ }
221
+ return {
222
+ application: assembly.application,
223
+ capabilities: assembly.capabilities.length,
224
+ releases: configValue.releases.length,
225
+ assemblyDigest: assembly.assemblyDigest
226
+ };
227
+ }
228
+ async function initializeCapability(options) {
229
+ if (!/^[a-z][a-z0-9-]*$/.test(options.namespace)) {
230
+ throw new Error("Capability namespace must use lowercase letters, numbers, and hyphens.");
231
+ }
232
+ const displayName = options.namespace.split("-").map((part, index) => index === 0 ? `${part.charAt(0).toUpperCase()}${part.slice(1)}` : part).join(" ");
233
+ const manifestPath = path.join(options.directory, "fabric.manifest.json");
234
+ const readmePath = path.join(options.directory, "README.md");
235
+ for (const file of [readmePath, manifestPath]) {
236
+ try {
237
+ await access(file);
238
+ throw new Error(`Refusing to overwrite existing file "${file}".`);
239
+ } catch (error) {
240
+ if (error instanceof Error && error.message.startsWith("Refusing to overwrite")) throw error;
241
+ if (error.code !== "ENOENT") throw error;
242
+ }
243
+ }
244
+ const manifest = {
245
+ manifestVersion: 2,
246
+ namespace: options.namespace,
247
+ objectTypes: [],
248
+ subjectTypes: [],
249
+ idPrefixes: [],
250
+ dependencies: [],
251
+ policies: [],
252
+ eventTypes: [],
253
+ actions: [],
254
+ views: [],
255
+ stateMachines: [],
256
+ capability: {
257
+ displayName,
258
+ description: `Governed ${displayName.toLowerCase()} capability.`,
259
+ stability: "experimental"
260
+ }
261
+ };
262
+ const readme = [
263
+ `# ${displayName}`,
264
+ "",
265
+ "Define domain objects, governed actions, events, and projections in `fabric.manifest.json`.",
266
+ "",
267
+ "```bash",
268
+ "fabric-gen --input fabric.manifest.json --out generated",
269
+ "fabric-gen --input fabric.manifest.json --out generated --check",
270
+ "```",
271
+ ""
272
+ ].join("\n");
273
+ await mkdir(options.directory, { recursive: true });
274
+ const created = [];
275
+ try {
276
+ for (const [file, content] of [
277
+ [readmePath, readme],
278
+ [manifestPath, `${JSON.stringify(manifest, null, 2)}
279
+ `]
280
+ ]) {
281
+ await writeFile(file, content, { encoding: "utf8", flag: "wx" });
282
+ created.push(file);
283
+ }
284
+ } catch (error) {
285
+ await Promise.all(created.map((file) => rm(file, { force: true })));
286
+ if (error.code === "EEXIST") {
287
+ throw new Error("Refusing to overwrite a file created during capability initialization.");
288
+ }
289
+ throw error;
290
+ }
291
+ return { created };
292
+ }
293
+ async function generateFromManifest(options) {
294
+ const input = JSON.parse(await readFile(options.inputPath, "utf8"));
295
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Manifest input must be a JSON object.");
296
+ const bundle = "modules" in input ? input : { manifestVersion: input.manifestVersion, modules: [input] };
297
+ const ir = compileManifestBundle(bundle);
298
+ const files = (await Promise.all(options.targets.map(async (name) => {
299
+ const target = GENERATOR_TARGETS[name];
300
+ if (!target) throw new Error(`Unknown generator target "${name}".`);
301
+ return (await runGenerator(target, ir)).map((file) => ({ ...file, path: `${name}/${file.path}` }));
302
+ }))).flat().sort((left, right) => compareCanonicalStrings(left.path, right.path));
303
+ const expected = new Set(files.map((file) => file.path));
304
+ const existing = await listFiles(options.outputDirectory);
305
+ const selectedPrefixes = options.targets.map((target) => `${target}/`);
306
+ const stale = existing.filter((file) => selectedPrefixes.some((prefix) => file.startsWith(prefix)) && !expected.has(file));
307
+ if (!options.check) await Promise.all(stale.map((file) => rm(path.join(options.outputDirectory, file), { force: true })));
308
+ const changed = [];
309
+ for (const file of files) {
310
+ const target = path.join(options.outputDirectory, file.path);
311
+ let current;
312
+ try {
313
+ current = await readFile(target, "utf8");
314
+ } catch {
315
+ current = void 0;
316
+ }
317
+ if (current !== file.content) {
318
+ changed.push(file.path);
319
+ if (!options.check) {
320
+ await mkdir(path.dirname(target), { recursive: true });
321
+ await writeFile(target, file.content, "utf8");
322
+ }
323
+ }
324
+ }
325
+ if (options.check && (changed.length || stale.length)) throw new Error(`Generated artifacts drifted: ${[...changed, ...stale.map((file) => `stale:${file}`)].join(", ")}`);
326
+ return { changed, stale, files };
327
+ }
328
+ async function listFiles(directory, prefix = "") {
329
+ let entries;
330
+ try {
331
+ entries = await readdir(directory, { withFileTypes: true });
332
+ } catch {
333
+ return [];
334
+ }
335
+ const nested = await Promise.all(entries.map((entry) => entry.isDirectory() ? listFiles(path.join(directory, entry.name), `${prefix}${entry.name}/`) : [`${prefix}${entry.name}`]));
336
+ return nested.flat().sort();
337
+ }
338
+ function assertExperiencePromotionConfig(value) {
339
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
340
+ throw new Error("Experience promotion configuration must be a JSON object.");
341
+ }
342
+ const config = value;
343
+ if (config.formatVersion !== 1) throw new Error("Experience promotion configuration formatVersion must be 1.");
344
+ for (const field of ["application", "issuer", "keyId", "assembly", "release"]) {
345
+ if (typeof config[field] !== "string" || !config[field]) {
346
+ throw new Error(`Experience promotion configuration ${field} must be a non-empty string.`);
347
+ }
348
+ }
349
+ if (!/^[a-z][a-z0-9-]*$/.test(config.application)) {
350
+ throw new Error("Experience promotion configuration application must be lowercase letters, numbers, and hyphens.");
351
+ }
352
+ if (!Array.isArray(config.contracts) || !config.contracts.every((entry) => typeof entry === "string" && entry)) {
353
+ throw new Error("Experience promotion configuration contracts must be an array of file paths.");
354
+ }
355
+ const known = /* @__PURE__ */ new Set(["formatVersion", "application", "issuer", "keyId", "assembly", "release", "contracts"]);
356
+ for (const field of Object.keys(config)) {
357
+ if (/key|secret|credential|token|pem|password/i.test(field) && field !== "keyId") {
358
+ throw new Error(`Experience promotion configuration must not carry "${field}"; pass the signing key with --key.`);
359
+ }
360
+ if (!known.has(field)) {
361
+ throw new Error(`Experience promotion configuration has an unknown field "${field}".`);
362
+ }
363
+ }
364
+ }
365
+ async function promoteExperience(options) {
366
+ const configValue = await readJson(options.configPath);
367
+ assertExperiencePromotionConfig(configValue);
368
+ const base = path.dirname(options.configPath);
369
+ const resolve = (file) => path.resolve(base, file);
370
+ const assembly = await readJson(resolve(configValue.assembly));
371
+ assertAssemblyLockfile(assembly);
372
+ const release = await readJson(resolve(configValue.release));
373
+ assertSduiReleaseIntegrity(release);
374
+ const contracts = await Promise.all(
375
+ configValue.contracts.map(async (file) => await readJson(resolve(file)))
376
+ );
377
+ const privateKey = createPrivateKey(await readFile(options.privateKeyPath, "utf8"));
378
+ const promoted = await promoteExperienceRelease({
379
+ application: configValue.application,
380
+ release,
381
+ contracts,
382
+ assembly,
383
+ issuer: configValue.issuer,
384
+ signer: createEs256ReleaseSigner({ keyId: configValue.keyId, privateKey })
385
+ });
386
+ const outputPath = options.outputPath ? path.resolve(options.outputPath) : resolve(`${configValue.application}.experience-release.jws`);
387
+ await mkdir(path.dirname(outputPath), { recursive: true });
388
+ await writeFile(outputPath, `${promoted.compactJws}
389
+ `, "utf8");
390
+ return {
391
+ application: configValue.application,
392
+ releaseDigest: release.releaseDigest,
393
+ assemblyDigest: assembly.assemblyDigest,
394
+ routes: {
395
+ views: promoted.release.routes.views.length,
396
+ intents: promoted.release.routes.intents.length
397
+ },
398
+ outputPath
399
+ };
400
+ }
401
+
402
+ export {
403
+ GENERATOR_TARGETS,
404
+ FabricGenUsageError,
405
+ fabricGenCliExitCode,
406
+ runFabricGenCommand,
407
+ initializeExperience,
408
+ checkApplication,
409
+ initializeCapability,
410
+ generateFromManifest,
411
+ promoteExperience
412
+ };
413
+ //# sourceMappingURL=chunk-4QFNFERM.js.map