@bedrock-rbx/core 0.1.0-beta.12 → 0.1.0-beta.13

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.
@@ -144,6 +144,7 @@ function deployErrorMessage(err) {
144
144
  case "applyFailed": return `apply failed for '${err.cause.key}': ${applyCauseDetail(err.cause)}`;
145
145
  case "buildDesiredFailed": return `build desired state failed for '${err.cause.key}' ${buildDesiredDetail(err.cause)}`;
146
146
  case "configLoadFailed": return `config load failed: ${configErrorDetail(err.cause)}`;
147
+ case "incompletePassEntry": return `pass '${err.key}' is missing '${err.missingField}' under environment '${err.environment}'`;
147
148
  case "incompletePlaceEntry": return `place '${err.key}' is missing '${err.missingField}' under environment '${err.environment}'`;
148
149
  case "incompleteUniverseEntry": return `universe is missing '${err.missingField}' under environment '${err.environment}'`;
149
150
  case "missingCredential": return `missing credential: environment variable ${err.variable} is not set`;
@@ -454,17 +455,255 @@ async function sha256Hex(bytes) {
454
455
  return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
455
456
  }
456
457
  //#endregion
458
+ //#region src/core/redacted-icon.ts
459
+ const REDACTED_ICON_BYTES = new Uint8Array([
460
+ 137,
461
+ 80,
462
+ 78,
463
+ 71,
464
+ 13,
465
+ 10,
466
+ 26,
467
+ 10,
468
+ 0,
469
+ 0,
470
+ 0,
471
+ 13,
472
+ 73,
473
+ 72,
474
+ 68,
475
+ 82,
476
+ 0,
477
+ 0,
478
+ 0,
479
+ 64,
480
+ 0,
481
+ 0,
482
+ 0,
483
+ 64,
484
+ 8,
485
+ 0,
486
+ 0,
487
+ 0,
488
+ 0,
489
+ 143,
490
+ 2,
491
+ 46,
492
+ 2,
493
+ 0,
494
+ 0,
495
+ 0,
496
+ 137,
497
+ 73,
498
+ 68,
499
+ 65,
500
+ 84,
501
+ 120,
502
+ 156,
503
+ 237,
504
+ 86,
505
+ 209,
506
+ 14,
507
+ 128,
508
+ 32,
509
+ 8,
510
+ 244,
511
+ 83,
512
+ 252,
513
+ 148,
514
+ 254,
515
+ 255,
516
+ 167,
517
+ 174,
518
+ 37,
519
+ 98,
520
+ 130,
521
+ 189,
522
+ 20,
523
+ 110,
524
+ 57,
525
+ 119,
526
+ 108,
527
+ 26,
528
+ 194,
529
+ 188,
530
+ 64,
531
+ 15,
532
+ 42,
533
+ 229,
534
+ 160,
535
+ 164,
536
+ 13,
537
+ 0,
538
+ 142,
539
+ 160,
540
+ 44,
541
+ 144,
542
+ 2,
543
+ 1,
544
+ 200,
545
+ 3,
546
+ 242,
547
+ 96,
548
+ 82,
549
+ 45,
550
+ 176,
551
+ 31,
552
+ 176,
553
+ 161,
554
+ 244,
555
+ 60,
556
+ 0,
557
+ 0,
558
+ 153,
559
+ 81,
560
+ 245,
561
+ 235,
562
+ 161,
563
+ 142,
564
+ 219,
565
+ 222,
566
+ 188,
567
+ 254,
568
+ 187,
569
+ 128,
570
+ 50,
571
+ 224,
572
+ 116,
573
+ 181,
574
+ 168,
575
+ 205,
576
+ 106,
577
+ 134,
578
+ 202,
579
+ 113,
580
+ 0,
581
+ 0,
582
+ 109,
583
+ 150,
584
+ 173,
585
+ 101,
586
+ 97,
587
+ 0,
588
+ 58,
589
+ 239,
590
+ 67,
591
+ 4,
592
+ 254,
593
+ 29,
594
+ 239,
595
+ 83,
596
+ 168,
597
+ 232,
598
+ 177,
599
+ 51,
600
+ 144,
601
+ 176,
602
+ 251,
603
+ 80,
604
+ 101,
605
+ 209,
606
+ 82,
607
+ 80,
608
+ 239,
609
+ 0,
610
+ 240,
611
+ 85,
612
+ 216,
613
+ 15,
614
+ 216,
615
+ 15,
616
+ 200,
617
+ 131,
618
+ 89,
619
+ 197,
620
+ 180,
621
+ 1,
622
+ 0,
623
+ 255,
624
+ 15,
625
+ 86,
626
+ 184,
627
+ 5,
628
+ 2,
629
+ 228,
630
+ 255,
631
+ 207,
632
+ 224,
633
+ 4,
634
+ 233,
635
+ 243,
636
+ 166,
637
+ 219,
638
+ 234,
639
+ 149,
640
+ 21,
641
+ 116,
642
+ 0,
643
+ 0,
644
+ 0,
645
+ 0,
646
+ 73,
647
+ 69,
648
+ 78,
649
+ 68,
650
+ 174,
651
+ 66,
652
+ 96,
653
+ 130
654
+ ]);
655
+ /**
656
+ * Sentinel path written into a resource's `icon["en-us"]` field when
657
+ * redaction substitutes the bedrock-supplied placeholder image. Callers
658
+ * route this path through {@link withRedactedIcon} or through the
659
+ * `readBytes` short-circuit; neither touches the filesystem.
660
+ */
661
+ const REDACTED_ICON_PATH = "<bedrock:redacted-icon.png>";
662
+ /**
663
+ * `true` when `path` is the redacted-icon sentinel. `readBytes` and
664
+ * {@link withRedactedIcon} both use this predicate to decide whether to
665
+ * bypass the injected file reader.
666
+ *
667
+ * @param path - Icon path supplied by a flattened or normalized resource entry.
668
+ * @returns `true` for {@link REDACTED_ICON_PATH}; otherwise `false`.
669
+ */
670
+ function isRedactedIconPath(path) {
671
+ return path === REDACTED_ICON_PATH;
672
+ }
673
+ /**
674
+ * Wrap a `readFile` so the sentinel resolves to {@link REDACTED_ICON_BYTES}
675
+ * without touching the inner reader. Applied once at the shell deploy /
676
+ * preview boundary; the wrapped reader flows to every consumer (normalize,
677
+ * registry drivers) unchanged.
678
+ *
679
+ * @param readFile - Inner reader that handles every non-sentinel path.
680
+ * @returns Sentinel-aware reader with the same callable shape as `readFile`.
681
+ */
682
+ function withRedactedIcon(readFile) {
683
+ return async (path) => {
684
+ if (isRedactedIconPath(path)) return new Uint8Array(REDACTED_ICON_BYTES);
685
+ return readFile(path);
686
+ };
687
+ }
688
+ //#endregion
457
689
  //#region src/core/kinds/read-bytes.ts
458
690
  /**
459
691
  * Read file bytes via the injected reader, translating rejections into a
460
692
  * `fileReadFailed` `BuildDesiredError`. Shared by kind modules whose
461
- * pre-I/O normalization hashes a file the user declared by path.
693
+ * pre-I/O normalization hashes a file the user declared by path. The
694
+ * redacted-icon sentinel short-circuits to the embedded placeholder
695
+ * bytes without invoking the injected reader, so a redaction-substituted
696
+ * icon path produces a deterministic hash on every deploy.
462
697
  *
463
698
  * @param target - Path to read plus the resource key blamed on failure.
464
699
  * @param io - I/O surface carrying the injected `readFile` function.
465
700
  * @returns `Ok` with the bytes, or `Err` with a `fileReadFailed` error.
466
701
  */
467
702
  async function readBytes(target, io) {
703
+ if (isRedactedIconPath(target.filePath)) return {
704
+ data: new Uint8Array(REDACTED_ICON_BYTES),
705
+ success: true
706
+ };
468
707
  try {
469
708
  return {
470
709
  data: await io.readFile(target.filePath),
@@ -696,12 +935,16 @@ function planFollowUpPatch(desired, createResponse) {
696
935
  * ```
697
936
  */
698
937
  function createDeveloperProductDriver(deps) {
938
+ const effective = {
939
+ ...deps,
940
+ readFile: withRedactedIcon(deps.readFile)
941
+ };
699
942
  return {
700
943
  async create(desired) {
701
- return createOne(deps, desired);
944
+ return createOne(effective, desired);
702
945
  },
703
946
  async update(current, desired) {
704
- return updateOne(deps, {
947
+ return updateOne(effective, {
705
948
  current,
706
949
  desired
707
950
  });
@@ -853,12 +1096,16 @@ async function updateOne(deps, { current, desired }) {
853
1096
  * ```
854
1097
  */
855
1098
  function createGamePassDriver(deps) {
1099
+ const effective = {
1100
+ ...deps,
1101
+ readFile: withRedactedIcon(deps.readFile)
1102
+ };
856
1103
  return {
857
1104
  async create(desired) {
858
- return createGamePass(deps, desired);
1105
+ return createGamePass(effective, desired);
859
1106
  },
860
1107
  async update(current, desired) {
861
- return updateGamePass(deps, {
1108
+ return updateGamePass(effective, {
862
1109
  current,
863
1110
  desired
864
1111
  });
@@ -1891,6 +2138,8 @@ function isGistStateConfig(config) {
1891
2138
  }
1892
2139
  const OPTIONAL_BOOLEAN$2 = "boolean | undefined";
1893
2140
  const OPTIONAL_STRING = "string | undefined";
2141
+ const REDACTED_KEY = "redacted?";
2142
+ const NON_EMPTY_OVERRIDE_MESSAGE = "a non-empty override object; use `redacted: true` for default placeholders";
1894
2143
  /**
1895
2144
  * Shared arktype constraint for any optional positive-integer field.
1896
2145
  * Reused by per-kind entry schemas so positive-integer fields validate
@@ -1907,11 +2156,35 @@ const OPTIONAL_POSITIVE_INTEGER = "(number.integer >= 1) | undefined";
1907
2156
  * identically.
1908
2157
  */
1909
2158
  const OPTIONAL_ROBUX_PRICE = "number.integer >= 0 | undefined";
2159
+ const gamePassRedacted = type({
2160
+ "description?": "string",
2161
+ "icon?": iconMap,
2162
+ "name?": "string"
2163
+ }).onUndeclaredKey("reject").narrow((value, ctx) => {
2164
+ if (Object.keys(value).length === 0) return ctx.mustBe(NON_EMPTY_OVERRIDE_MESSAGE);
2165
+ return true;
2166
+ }).or(OPTIONAL_BOOLEAN$2);
2167
+ const placeRedacted = type({
2168
+ "description?": "string",
2169
+ "displayName?": "string"
2170
+ }).onUndeclaredKey("reject").narrow((value, ctx) => {
2171
+ if (Object.keys(value).length === 0) return ctx.mustBe(NON_EMPTY_OVERRIDE_MESSAGE);
2172
+ return true;
2173
+ }).or(OPTIONAL_BOOLEAN$2);
2174
+ const productRedacted = type({
2175
+ "description?": "string",
2176
+ "icon?": iconMap,
2177
+ "name?": "string"
2178
+ }).onUndeclaredKey("reject").narrow((value, ctx) => {
2179
+ if (Object.keys(value).length === 0) return ctx.mustBe(NON_EMPTY_OVERRIDE_MESSAGE);
2180
+ return true;
2181
+ }).or(OPTIONAL_BOOLEAN$2);
1910
2182
  const gamePassEntry = type({
1911
2183
  "name": "string",
1912
2184
  "description": "string",
1913
2185
  "icon": iconMap,
1914
- "price?": OPTIONAL_ROBUX_PRICE
2186
+ "price?": OPTIONAL_ROBUX_PRICE,
2187
+ [REDACTED_KEY]: gamePassRedacted
1915
2188
  });
1916
2189
  const passesCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: gamePassEntry }).onUndeclaredKey("reject");
1917
2190
  const developerProductEntry = type({
@@ -1920,6 +2193,7 @@ const developerProductEntry = type({
1920
2193
  "icon?": iconMap,
1921
2194
  "isRegionalPricingEnabled?": OPTIONAL_BOOLEAN$2,
1922
2195
  "price?": OPTIONAL_ROBUX_PRICE,
2196
+ [REDACTED_KEY]: productRedacted,
1923
2197
  "storePageEnabled?": OPTIONAL_BOOLEAN$2
1924
2198
  }).onUndeclaredKey("reject");
1925
2199
  const productsCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: developerProductEntry }).onUndeclaredKey("reject");
@@ -1928,6 +2202,7 @@ const placeEntry = type({
1928
2202
  "description?": OPTIONAL_STRING,
1929
2203
  "displayName?": OPTIONAL_STRING,
1930
2204
  "filePath": "string",
2205
+ [REDACTED_KEY]: placeRedacted,
1931
2206
  "serverSize?": OPTIONAL_POSITIVE_INTEGER
1932
2207
  }).onUndeclaredKey("reject");
1933
2208
  const placesCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: placeEntry }).onUndeclaredKey("reject");
@@ -1961,7 +2236,8 @@ const gamePassOverlay = type({
1961
2236
  "description?": "string",
1962
2237
  "icon?": iconMap,
1963
2238
  "name?": "string",
1964
- "price?": OPTIONAL_ROBUX_PRICE
2239
+ "price?": OPTIONAL_ROBUX_PRICE,
2240
+ [REDACTED_KEY]: OPTIONAL_BOOLEAN$2
1965
2241
  }).onUndeclaredKey("reject");
1966
2242
  const passesOverlayCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: gamePassOverlay }).onUndeclaredKey("reject");
1967
2243
  const developerProductOverlay = type({
@@ -1970,6 +2246,7 @@ const developerProductOverlay = type({
1970
2246
  "isRegionalPricingEnabled?": OPTIONAL_BOOLEAN$2,
1971
2247
  "name?": "string",
1972
2248
  "price?": OPTIONAL_ROBUX_PRICE,
2249
+ [REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
1973
2250
  "storePageEnabled?": OPTIONAL_BOOLEAN$2
1974
2251
  }).onUndeclaredKey("reject");
1975
2252
  const productsOverlayCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: developerProductOverlay }).onUndeclaredKey("reject");
@@ -1978,15 +2255,19 @@ const placeOverlay = type({
1978
2255
  "displayName?": OPTIONAL_STRING,
1979
2256
  "filePath?": "string",
1980
2257
  "placeId": ROBLOX_ID_DIGITS,
2258
+ [REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
1981
2259
  "serverSize?": OPTIONAL_POSITIVE_INTEGER
1982
2260
  }).onUndeclaredKey("reject");
2261
+ const placesOverlayCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: placeOverlay }).onUndeclaredKey("reject");
2262
+ const universeOverlay = universeEntry;
1983
2263
  const environmentEntry = type({
1984
2264
  "label?": OPTIONAL_STRING,
1985
2265
  "passes?": passesOverlayCollection,
1986
- "places?": type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: placeOverlay }).onUndeclaredKey("reject"),
2266
+ "places?": placesOverlayCollection,
1987
2267
  "products?": productsOverlayCollection,
2268
+ [REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
1988
2269
  "state?": stateConfig,
1989
- "universe?": universeEntry
2270
+ "universe?": universeOverlay
1990
2271
  }).onUndeclaredKey("reject");
1991
2272
  const rootSchema = type({
1992
2273
  "displayNamePrefix?": type({
@@ -2083,6 +2364,7 @@ const entrySchema$3 = type({
2083
2364
  "icon?": iconMap,
2084
2365
  "isRegionalPricingEnabled?": OPTIONAL_BOOLEAN$1,
2085
2366
  "price?": OPTIONAL_ROBUX_PRICE,
2367
+ "redacted?": "boolean | undefined",
2086
2368
  "storePageEnabled?": OPTIONAL_BOOLEAN$1
2087
2369
  });
2088
2370
  function flatten$3(config) {
@@ -2166,7 +2448,8 @@ const entrySchema$2 = type({
2166
2448
  "name": "string",
2167
2449
  "description": "string",
2168
2450
  "icon": iconMap,
2169
- "price?": OPTIONAL_ROBUX_PRICE
2451
+ "price?": OPTIONAL_ROBUX_PRICE,
2452
+ "redacted?": "boolean | undefined"
2170
2453
  });
2171
2454
  function flatten$2(config) {
2172
2455
  return Object.entries(config.passes ?? {}).map(([key, entry]) => {
@@ -2647,9 +2930,172 @@ function resolveStateConfig(config, environment) {
2647
2930
  success: false
2648
2931
  };
2649
2932
  }
2933
+ /**
2934
+ * Pure transform that substitutes bedrock-supplied placeholder content for
2935
+ * every resource whose effective `redacted` flag is truthy. The effective
2936
+ * flag is the per-resource `redacted` value when set, otherwise the
2937
+ * `environmentRedacted` fallback. A `redacted` object form replaces
2938
+ * matching fields with the supplied values and falls back to the bedrock
2939
+ * defaults for the rest. Runs between env-overlay merge and display-name
2940
+ * prefix render so the rest of the pipeline (flatten, normalize, diff,
2941
+ * apply) operates on already-redacted values and needs no special-case
2942
+ * redaction logic.
2943
+ *
2944
+ * @param config - Post-merge `ResolvedConfig` produced by `selectEnvironment`.
2945
+ * @param environmentRedacted - Environment-level redaction toggle. Resources
2946
+ * that omit a per-resource `redacted` flag inherit this value.
2947
+ * @returns A `ResolvedConfig` whose redacted entries carry placeholder
2948
+ * values; non-redacted entries pass through verbatim, and the input is
2949
+ * not mutated.
2950
+ */
2951
+ function applyRedaction(config, environmentRedacted = false) {
2952
+ const passes = redactPasses(config.passes, environmentRedacted);
2953
+ const places = redactPlaces(config.places, environmentRedacted);
2954
+ const products = redactProducts(config.products, environmentRedacted);
2955
+ if (passes === config.passes && places === config.places && products === config.products) return config;
2956
+ return {
2957
+ ...config,
2958
+ ...passes === void 0 ? {} : { passes },
2959
+ ...places === void 0 ? {} : { places },
2960
+ ...products === void 0 ? {} : { products }
2961
+ };
2962
+ }
2963
+ /**
2964
+ * Inspect the pre-redaction merged config and produce one annotation per
2965
+ * resource flagged `redacted: true`. Callers thread the result into plan
2966
+ * output so authors can see which resources are redacted in the active
2967
+ * environment and whether their real-value edits are being suppressed.
2968
+ *
2969
+ * Operates on the pre-redaction view because the post-redaction config no
2970
+ * longer carries the real `name`/`description`/`icon` values needed to
2971
+ * detect divergence from the placeholder defaults.
2972
+ *
2973
+ * @param merged - `ResolvedConfig` produced by environment overlay merge,
2974
+ * before `applyRedaction` has substituted placeholders.
2975
+ * @returns Zero or more annotations, one per redacted resource. Empty when
2976
+ * the config declares no redacted resources.
2977
+ */
2978
+ function collectRedactionAnnotations(merged) {
2979
+ const passes = Object.entries(merged.passes ?? {}).filter(([, entry]) => entry.redacted === true).map(([key, entry]) => {
2980
+ return {
2981
+ key: asResourceKey(key),
2982
+ hasRealValueEdits: passHasRealValueEdits(entry),
2983
+ kind: "gamePass"
2984
+ };
2985
+ });
2986
+ const products = Object.entries(merged.products ?? {}).filter(([, entry]) => entry.redacted === true).map(([key, entry]) => {
2987
+ return {
2988
+ key: asResourceKey(key),
2989
+ hasRealValueEdits: productHasRealValueEdits(entry),
2990
+ kind: "developerProduct"
2991
+ };
2992
+ });
2993
+ return [...passes, ...products];
2994
+ }
2995
+ function redactPass(entry, override) {
2996
+ return {
2997
+ ...entry,
2998
+ name: override.name ?? "Redacted Pass",
2999
+ description: override.description ?? "",
3000
+ icon: override.icon ?? { "en-us": "<bedrock:redacted-icon.png>" }
3001
+ };
3002
+ }
3003
+ function redactPasses(passes, environmentRedacted) {
3004
+ if (passes === void 0) return;
3005
+ if (!Object.values(passes).some((entry) => (entry.redacted ?? environmentRedacted) !== false)) return passes;
3006
+ return Object.fromEntries(Object.entries(passes).map(([key, entry]) => {
3007
+ const effective = entry.redacted ?? environmentRedacted;
3008
+ if (effective === false) return [key, entry];
3009
+ return [key, redactPass(entry, typeof effective === "object" ? effective : {})];
3010
+ }));
3011
+ }
3012
+ function redactPlace(entry, override) {
3013
+ return {
3014
+ ...entry,
3015
+ description: override.description ?? "",
3016
+ displayName: override.displayName ?? entry.displayName
3017
+ };
3018
+ }
3019
+ function redactPlaces(places, environmentRedacted) {
3020
+ if (places === void 0) return;
3021
+ if (!Object.values(places).some((entry) => (entry.redacted ?? environmentRedacted) !== false)) return places;
3022
+ return Object.fromEntries(Object.entries(places).map(([key, entry]) => {
3023
+ const effective = entry.redacted ?? environmentRedacted;
3024
+ if (effective === false) return [key, entry];
3025
+ return [key, redactPlace(entry, typeof effective === "object" ? effective : {})];
3026
+ }));
3027
+ }
3028
+ function redactProduct(entry, override) {
3029
+ return {
3030
+ ...entry,
3031
+ name: override.name ?? "Redacted Product",
3032
+ description: override.description ?? "",
3033
+ icon: override.icon ?? { "en-us": "<bedrock:redacted-icon.png>" }
3034
+ };
3035
+ }
3036
+ function redactProducts(products, environmentRedacted) {
3037
+ if (products === void 0) return;
3038
+ if (!Object.values(products).some((entry) => (entry.redacted ?? environmentRedacted) !== false)) return products;
3039
+ return Object.fromEntries(Object.entries(products).map(([key, entry]) => {
3040
+ const effective = entry.redacted ?? environmentRedacted;
3041
+ if (effective === false) return [key, entry];
3042
+ return [key, redactProduct(entry, typeof effective === "object" ? effective : {})];
3043
+ }));
3044
+ }
3045
+ function passHasRealValueEdits(entry) {
3046
+ return entry.name !== "Redacted Pass" || entry.description !== "" || entry.icon["en-us"] !== "<bedrock:redacted-icon.png>";
3047
+ }
3048
+ function productHasRealValueEdits(entry) {
3049
+ return entry.name !== "Redacted Product" || entry.description !== "" || entry.icon !== void 0 && entry.icon["en-us"] !== "<bedrock:redacted-icon.png>";
3050
+ }
2650
3051
  //#endregion
2651
3052
  //#region src/core/select-environment.ts
2652
3053
  /**
3054
+ * Project a `Config` onto a single environment up to the pre-redaction
3055
+ * merge boundary. Looks up the env entry, deep-merges its resource overlay
3056
+ * over the root config, and runs the same pass, place, and universe
3057
+ * completeness checks {@link selectEnvironment} runs, so the returned
3058
+ * `merged` config honours the full `ResolvedConfig` contract. Real
3059
+ * `name`, `description`, and `icon` values on redacted resources stay
3060
+ * intact, letting callers inspect divergence from placeholder defaults
3061
+ * before {@link selectEnvironment} substitutes them.
3062
+ *
3063
+ * @param config - Validated project config.
3064
+ * @param environment - Environment name to project onto.
3065
+ * @returns The matched env entry plus the merged config, or any of the
3066
+ * `SelectEnvironmentError` failure modes.
3067
+ */
3068
+ function selectMergedEnvironment(config, environment) {
3069
+ const entry = config.environments[environment];
3070
+ if (entry === void 0) return {
3071
+ err: unknownEnvironment(config, environment),
3072
+ success: false
3073
+ };
3074
+ const merged = mergeOverlays(config, entry);
3075
+ const incompletePass = findIncompletePass(merged, environment);
3076
+ if (incompletePass !== void 0) return {
3077
+ err: incompletePass,
3078
+ success: false
3079
+ };
3080
+ const incompletePlace = findIncompletePlace(merged, environment);
3081
+ if (incompletePlace !== void 0) return {
3082
+ err: incompletePlace,
3083
+ success: false
3084
+ };
3085
+ const incompleteUniverse = findIncompleteUniverse(merged, environment);
3086
+ if (incompleteUniverse !== void 0) return {
3087
+ err: incompleteUniverse,
3088
+ success: false
3089
+ };
3090
+ return {
3091
+ data: {
3092
+ entry,
3093
+ merged
3094
+ },
3095
+ success: true
3096
+ };
3097
+ }
3098
+ /**
2653
3099
  * Project a validated `Config` onto a single environment. Looks up the
2654
3100
  * matching `environments[environment]` entry, deep-merges its resource
2655
3101
  * overlay (`passes`, `places`, `universe`) over the root config via defu,
@@ -2727,28 +3173,80 @@ function resolveStateConfig(config, environment) {
2727
3173
  * projection failed.
2728
3174
  */
2729
3175
  function selectEnvironment(config, environment) {
2730
- const entry = config.environments[environment];
2731
- if (entry === void 0) return {
2732
- err: unknownEnvironment(config, environment),
2733
- success: false
3176
+ const mergedResult = selectMergedEnvironment(config, environment);
3177
+ if (!mergedResult.success) return mergedResult;
3178
+ const { entry, merged } = mergedResult.data;
3179
+ return {
3180
+ data: redactAndPrefix({
3181
+ config,
3182
+ entry,
3183
+ merged
3184
+ }),
3185
+ success: true
2734
3186
  };
2735
- const projected = projectConfig({
2736
- config,
2737
- entry
2738
- });
2739
- const incompletePlace = findIncompletePlace(projected, environment);
2740
- if (incompletePlace !== void 0) return {
2741
- err: incompletePlace,
2742
- success: false
3187
+ }
3188
+ function findIncompletePass(merged, environment) {
3189
+ const { passes } = merged;
3190
+ if (passes === void 0) return;
3191
+ const candidates = passes;
3192
+ for (const [key, entry] of Object.entries(candidates)) {
3193
+ if (entry.name === void 0) return {
3194
+ key,
3195
+ environment,
3196
+ kind: "incompletePassEntry",
3197
+ missingField: "name"
3198
+ };
3199
+ if (entry.description === void 0) return {
3200
+ key,
3201
+ environment,
3202
+ kind: "incompletePassEntry",
3203
+ missingField: "description"
3204
+ };
3205
+ if (entry.icon === void 0) return {
3206
+ key,
3207
+ environment,
3208
+ kind: "incompletePassEntry",
3209
+ missingField: "icon"
3210
+ };
3211
+ }
3212
+ }
3213
+ function mergeEntry(overlay, base) {
3214
+ return defu(overlay, base ?? {});
3215
+ }
3216
+ function mergeKeyedRecord(overlay, base) {
3217
+ if (overlay === void 0) return base;
3218
+ return {
3219
+ ...base ?? {},
3220
+ ...Object.fromEntries(Object.entries(overlay).map(([key, partial]) => {
3221
+ return [key, mergeEntry(partial, base?.[key])];
3222
+ }))
2743
3223
  };
2744
- const incompleteUniverse = findIncompleteUniverse(projected, environment);
2745
- if (incompleteUniverse !== void 0) return {
2746
- err: incompleteUniverse,
2747
- success: false
3224
+ }
3225
+ function mergeUniverse(overlay, base) {
3226
+ if (overlay === void 0 && base === void 0) return;
3227
+ return defu(overlay ?? {}, base ?? {});
3228
+ }
3229
+ function mergeOverlays(config, entry) {
3230
+ const passes = mergeKeyedRecord(entry.passes, config.passes);
3231
+ const places = mergeKeyedRecord(entry.places, config.places);
3232
+ const products = mergeKeyedRecord(entry.products, config.products);
3233
+ const universe = mergeUniverse(entry.universe, config.universe);
3234
+ const state = entry.state ?? config.state;
3235
+ const { places: _placesRoot, products: _productsRoot, universe: _universeRoot, ...rest } = config;
3236
+ return {
3237
+ ...rest,
3238
+ ...passes === void 0 ? {} : { passes },
3239
+ ...places === void 0 ? {} : { places },
3240
+ ...products === void 0 ? {} : { products },
3241
+ ...state === void 0 ? {} : { state },
3242
+ ...universe === void 0 ? {} : { universe }
2748
3243
  };
3244
+ }
3245
+ function unknownEnvironment(config, environment) {
2749
3246
  return {
2750
- data: projected,
2751
- success: true
3247
+ declared: Object.keys(config.environments),
3248
+ environment,
3249
+ kind: "unknownEnvironment"
2752
3250
  };
2753
3251
  }
2754
3252
  function findIncompleteUniverse(projected, environment) {
@@ -2779,22 +3277,6 @@ function findIncompletePlace(projected, environment) {
2779
3277
  };
2780
3278
  }
2781
3279
  }
2782
- function mergeEntry(overlay, base) {
2783
- return defu(overlay, base ?? {});
2784
- }
2785
- function mergeKeyedRecord(overlay, base) {
2786
- if (overlay === void 0) return base;
2787
- return {
2788
- ...base ?? {},
2789
- ...Object.fromEntries(Object.entries(overlay).map(([key, partial]) => {
2790
- return [key, mergeEntry(partial, base?.[key])];
2791
- }))
2792
- };
2793
- }
2794
- function mergeUniverse(overlay, base) {
2795
- if (overlay === void 0 && base === void 0) return;
2796
- return defu(overlay ?? {}, base ?? {});
2797
- }
2798
3280
  function resolvePrefix(config, entry) {
2799
3281
  if (config.displayNamePrefix?.enabled === false) return;
2800
3282
  const { label } = entry;
@@ -2818,33 +3300,18 @@ function applyPlacesPrefix(places, prefix) {
2818
3300
  }];
2819
3301
  }));
2820
3302
  }
2821
- function projectConfig(inputs) {
2822
- const { config, entry } = inputs;
2823
- const passes = mergeKeyedRecord(entry.passes, config.passes);
2824
- const mergedPlaces = mergeKeyedRecord(entry.places, config.places);
2825
- const products = mergeKeyedRecord(entry.products, config.products);
2826
- const merged = mergeUniverse(entry.universe, config.universe);
3303
+ function redactAndPrefix(inputs) {
3304
+ const { config, entry, merged } = inputs;
3305
+ const redacted = applyRedaction(merged, entry.redacted);
2827
3306
  const prefix = resolvePrefix(config, entry);
2828
- const universe = applyUniversePrefix(merged, prefix);
2829
- const places = applyPlacesPrefix(mergedPlaces, prefix);
2830
- const state = entry.state ?? config.state;
2831
- const { places: _placesRoot, products: _productsRoot, universe: _universeRoot, ...rest } = config;
3307
+ const places = applyPlacesPrefix(redacted.places, prefix);
3308
+ const universe = applyUniversePrefix(redacted.universe, prefix);
2832
3309
  return {
2833
- ...rest,
2834
- ...passes === void 0 ? {} : { passes },
3310
+ ...redacted,
2835
3311
  ...places === void 0 ? {} : { places },
2836
- ...products === void 0 ? {} : { products },
2837
- ...state === void 0 ? {} : { state },
2838
3312
  ...universe === void 0 ? {} : { universe }
2839
3313
  };
2840
3314
  }
2841
- function unknownEnvironment(config, environment) {
2842
- return {
2843
- declared: Object.keys(config.environments),
2844
- environment,
2845
- kind: "unknownEnvironment"
2846
- };
2847
- }
2848
3315
  //#endregion
2849
3316
  //#region src/core/validate-plan.ts
2850
3317
  /**
@@ -4462,6 +4929,14 @@ function buildRootPasses(primaryFold) {
4462
4929
  if (primaryFold.passes.length === 0) return;
4463
4930
  return Object.fromEntries(primaryFold.passes.map(({ key, entry }) => [key, entry]));
4464
4931
  }
4932
+ function buildFullPassOverlay(entry) {
4933
+ return {
4934
+ name: entry.name,
4935
+ description: entry.description,
4936
+ icon: entry.icon,
4937
+ ...entry.price !== void 0 && { price: entry.price }
4938
+ };
4939
+ }
4465
4940
  function buildPassOverlayEntry(entry, primary) {
4466
4941
  const overlay = {};
4467
4942
  if (!Object.is(primary.name, entry.name)) overlay.name = entry.name;
@@ -4475,7 +4950,7 @@ function buildPassesOverlay(fold, primary) {
4475
4950
  const overlay = {};
4476
4951
  for (const { key, entry } of fold.passes) {
4477
4952
  const primaryEntry = primaryByKey.get(key);
4478
- const passOverlay = primaryEntry === void 0 ? { ...entry } : buildPassOverlayEntry(entry, primaryEntry);
4953
+ const passOverlay = primaryEntry === void 0 ? buildFullPassOverlay(entry) : buildPassOverlayEntry(entry, primaryEntry);
4479
4954
  if (passOverlay !== void 0) overlay[key] = passOverlay;
4480
4955
  }
4481
4956
  return Object.keys(overlay).length === 0 ? void 0 : overlay;
@@ -6038,6 +6513,6 @@ function isFileMissing(err) {
6038
6513
  return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && FILE_MISSING_CODES.has(err.code);
6039
6514
  }
6040
6515
  //#endregion
6041
- export { shouldReuploadIcon as A, renderDeployError as B, UNIVERSE_SINGLETON_KEY as C, validateEnvironmentName as D, serializeStateFile as E, isRobloxAssetId as F, renderStateWriteError as G, renderMigrateParseError as H, isSha256Hex as I, derivePriceFields as L, asRobloxAssetId as M, asSha256Hex as N, createGamePassDriver as O, isResourceKey as P, createClackProgressAdapter as R, SOCIAL_LINK_FIELDS as S, parseStateFile as T, renderMigrationSummary as U, renderMigrateError as V, renderParseError as W, isGistStateConfig as _, buildStatePort as a, createUniverseDriver as b, applyOps as c, resolveStateConfig as d, flattenConfig as f, defaultKindRegistry as g, diff as h, loadConfig$1 as i, asResourceKey as j, createDeveloperProductDriver as k, validatePlan as l, renderDisplayNamePrefix as m, serializeConfig as n, buildDesired as o, DEFAULT_PREFIX_FORMAT as p, deploy as r, buildDefaultRegistry as s, migrateMantleState as t, selectEnvironment as u, validateConfig as v, createGistStateAdapter as w, createPlaceDriver as x, createClackPort as y, renderBuildStatePortError as z };
6516
+ export { createGamePassDriver as A, createClackProgressAdapter as B, createPlaceDriver as C, parseStateFile as D, createGistStateAdapter as E, asSha256Hex as F, renderMigrationSummary as G, renderDeployError as H, isResourceKey as I, renderParseError as K, isRobloxAssetId as L, shouldReuploadIcon as M, asResourceKey as N, serializeStateFile as O, asRobloxAssetId as P, isSha256Hex as R, createUniverseDriver as S, UNIVERSE_SINGLETON_KEY as T, renderMigrateError as U, renderBuildStatePortError as V, renderMigrateParseError as W, diff as _, buildStatePort as a, validateConfig as b, applyOps as c, selectMergedEnvironment as d, collectRedactionAnnotations as f, renderDisplayNamePrefix as g, DEFAULT_PREFIX_FORMAT as h, loadConfig$1 as i, createDeveloperProductDriver as j, validateEnvironmentName as k, validatePlan as l, flattenConfig as m, serializeConfig as n, buildDesired as o, resolveStateConfig as p, renderStateWriteError as q, deploy as r, buildDefaultRegistry as s, migrateMantleState as t, selectEnvironment as u, defaultKindRegistry as v, SOCIAL_LINK_FIELDS as w, createClackPort as x, isGistStateConfig as y, derivePriceFields as z };
6042
6517
 
6043
- //# sourceMappingURL=migrate-mantle-state-Dkk5zGHw.mjs.map
6518
+ //# sourceMappingURL=migrate-mantle-state-CQjWBZwT.mjs.map