@bedrock-rbx/core 0.1.0-beta.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.
@@ -0,0 +1,3449 @@
1
+ import { C as ConfigError, S as validateConfig, _ as StateConfig, a as ConfigEnvironmentUniverseId, b as UniverseOverlayWithoutId, c as DisplayNamePrefixConfig, d as GistStateConfig, f as PlaceEntry, g as ResourceEntryByKind, h as ResolvedUniverseEntry, i as Config, l as EnvironmentEntry, m as ResolvedPlaceEntry, n as ConfigInput, o as ConfigRootUniverseId, p as ResolvedConfig, r as defineConfig, s as DeveloperProductEntry, t as ConfigContext, u as GamePassEntry, v as UniverseEntry, w as ConfigValidationIssue, x as isGistStateConfig, y as UniverseOverlayWithId } from "./define-config-D-LAhfSJ.mjs";
2
+ import { Type as Type$1 } from "arktype";
3
+ import { OpenCloudError, OpenCloudError as OpenCloudError$1, Result, Result as Result$1 } from "@bedrock-rbx/ocale";
4
+ import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
5
+ import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
6
+ import { PlacesClient } from "@bedrock-rbx/ocale/places";
7
+ import { SocialLink, SocialLink as SocialLink$1, UniversesClient } from "@bedrock-rbx/ocale/universes";
8
+
9
+ //#region ../../node_modules/.pnpm/tagged-tag@1.0.0/node_modules/tagged-tag/index.d.ts
10
+ declare const tag: unique symbol;
11
+ //#endregion
12
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/tagged.d.ts
13
+ // eslint-disable-next-line type-fest/require-exported-types
14
+ type TagContainer<Token> = {
15
+ readonly [tag]: Token;
16
+ };
17
+ type Tag<Token extends PropertyKey, TagMetadata> = TagContainer<{ [K in Token]: TagMetadata }>;
18
+ /**
19
+ Create a [tagged type](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d) that can support [multiple tags](https://github.com/sindresorhus/type-fest/issues/665) and [per-tag metadata](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf).
20
+
21
+ A type returned by `Tagged` can be passed to `Tagged` again, to create a type with multiple tags.
22
+
23
+ A tag's name is usually a string (and must be a string, number, or symbol), but each application of a tag can also contain an arbitrary type as its "metadata". See {@link GetTagMetadata} for examples and explanation.
24
+
25
+ A type `A` returned by `Tagged` is assignable to another type `B` returned by `Tagged` if and only if:
26
+ - the underlying (untagged) type of `A` is assignable to the underlying type of `B`;
27
+ - `A` contains at least all the tags `B` has;
28
+ - and the metadata type for each of `A`'s tags is assignable to the metadata type of `B`'s corresponding tag.
29
+
30
+ There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
31
+ - [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
32
+ - [Microsoft/TypeScript#4895](https://github.com/microsoft/TypeScript/issues/4895)
33
+ - [Microsoft/TypeScript#33290](https://github.com/microsoft/TypeScript/pull/33290)
34
+
35
+ @example
36
+ ```
37
+ import type {Tagged} from 'type-fest';
38
+
39
+ type AccountNumber = Tagged<number, 'AccountNumber'>;
40
+ type AccountBalance = Tagged<number, 'AccountBalance'>;
41
+
42
+ function createAccountNumber(): AccountNumber {
43
+ // As you can see, casting from a `number` (the underlying type being tagged) is allowed.
44
+ return 2 as AccountNumber;
45
+ }
46
+
47
+ declare function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance;
48
+
49
+ // This will compile successfully.
50
+ getMoneyForAccount(createAccountNumber());
51
+
52
+ // But this won't, because it has to be explicitly passed as an `AccountNumber` type!
53
+ // Critically, you could not accidentally use an `AccountBalance` as an `AccountNumber`.
54
+ // @ts-expect-error
55
+ getMoneyForAccount(2);
56
+
57
+ // You can also use tagged values like their underlying, untagged type.
58
+ // I.e., this will compile successfully because an `AccountNumber` can be used as a regular `number`.
59
+ // In this sense, the underlying base type is not hidden, which differentiates tagged types from opaque types in other languages.
60
+ const accountNumber = createAccountNumber() + 2;
61
+ ```
62
+
63
+ @example
64
+ ```
65
+ import type {Tagged} from 'type-fest';
66
+
67
+ // You can apply multiple tags to a type by using `Tagged` repeatedly.
68
+ type Url = Tagged<string, 'URL'>;
69
+ type SpecialCacheKey = Tagged<Url, 'SpecialCacheKey'>;
70
+
71
+ // You can also pass a union of tag names, so this is equivalent to the above, although it doesn't give you the ability to assign distinct metadata to each tag.
72
+ type SpecialCacheKey2 = Tagged<string, 'URL' | 'SpecialCacheKey'>;
73
+ ```
74
+
75
+ @category Type
76
+ */
77
+ type Tagged<Type, TagName extends PropertyKey, TagMetadata = never> = Type & Tag<TagName, TagMetadata>;
78
+ //#endregion
79
+ //#region src/types/ids.d.ts
80
+ /**
81
+ * User-supplied identifier for a resource within a config (e.g. `"vip-pass"`).
82
+ * Stable across deploys; used to correlate desired ↔ current state.
83
+ */
84
+ type ResourceKey = Tagged<string, "ResourceKey">;
85
+ /**
86
+ * Roblox-assigned numeric asset ID, represented as a string to avoid int64
87
+ * precision loss in JavaScript.
88
+ */
89
+ type RobloxAssetId = Tagged<string, "RobloxAssetId">;
90
+ /**
91
+ * Lowercase hex-encoded SHA-256 digest (exactly 64 characters drawn from
92
+ * `0-9a-f`). Used to detect changes to file-backed resource inputs such as
93
+ * game-pass icons without re-uploading the file.
94
+ */
95
+ type Sha256Hex = Tagged<string, "Sha256Hex">;
96
+ /**
97
+ * Type predicate: test whether a raw string is a valid {@link ResourceKey}.
98
+ *
99
+ * Prefer this when the caller owns error handling (for example, constructing a
100
+ * `Result` error in a shell-layer parser). Use {@link asResourceKey} when an
101
+ * exception is the right failure mode.
102
+ *
103
+ * @example
104
+ *
105
+ * ```ts
106
+ * import { isResourceKey } from "@bedrock-rbx/core";
107
+ *
108
+ * const valid = isResourceKey("vip-pass");
109
+ * const invalid = isResourceKey("vip pass");
110
+ * expect(valid).toBe(true);
111
+ * expect(invalid).toBe(false);
112
+ * ```
113
+ *
114
+ * @param raw - String to test.
115
+ * @returns `true` when `raw` matches the ResourceKey shape; narrows `raw`.
116
+ */
117
+ declare function isResourceKey(raw: string): raw is ResourceKey;
118
+ /**
119
+ * Validate and brand a raw string as a {@link ResourceKey}.
120
+ *
121
+ * Accepts non-empty strings of alphanumeric characters, hyphens, and
122
+ * underscores (matching `/^[A-Za-z0-9_-]+$/`).
123
+ *
124
+ * @example
125
+ *
126
+ * ```ts
127
+ * import { asResourceKey } from "@bedrock-rbx/core";
128
+ *
129
+ * const key = asResourceKey("vip-pass");
130
+ *
131
+ * let thrown: unknown;
132
+ * try {
133
+ * asResourceKey("vip pass");
134
+ * } catch (error) {
135
+ * thrown = error;
136
+ * }
137
+ *
138
+ * expect(key).toBe("vip-pass");
139
+ * expect(thrown).toBeInstanceOf(RangeError);
140
+ * ```
141
+ *
142
+ * @param raw - Raw string to validate and brand.
143
+ * @returns The input re-typed as a {@link ResourceKey}.
144
+ * @throws RangeError when `raw` is empty or contains disallowed characters.
145
+ */
146
+ declare function asResourceKey(raw: string): ResourceKey;
147
+ /**
148
+ * Type predicate: test whether a raw string is a valid {@link RobloxAssetId}.
149
+ *
150
+ * Prefer this when the caller owns error handling (for example, constructing a
151
+ * `Result` error in a shell-layer parser). Use {@link asRobloxAssetId} when an
152
+ * exception is the right failure mode.
153
+ *
154
+ * @example
155
+ *
156
+ * ```ts
157
+ * import { isRobloxAssetId } from "@bedrock-rbx/core";
158
+ *
159
+ * const valid = isRobloxAssetId("12345");
160
+ * const invalid = isRobloxAssetId("12345abc");
161
+ * expect(valid).toBe(true);
162
+ * expect(invalid).toBe(false);
163
+ * ```
164
+ *
165
+ * @param raw - String to test.
166
+ * @returns `true` when `raw` matches the RobloxAssetId shape; narrows `raw`.
167
+ */
168
+ declare function isRobloxAssetId(raw: string): raw is RobloxAssetId;
169
+ /**
170
+ * Validate and brand a raw string as a {@link RobloxAssetId}.
171
+ *
172
+ * Accepts non-empty digit-only strings (matching `/^\d+$/`). Roblox asset IDs
173
+ * are int64 values carried as strings because JavaScript's `number` cannot
174
+ * represent the full int64 range.
175
+ *
176
+ * @example
177
+ *
178
+ * ```ts
179
+ * import { asRobloxAssetId } from "@bedrock-rbx/core";
180
+ *
181
+ * const id = asRobloxAssetId("12345");
182
+ *
183
+ * let thrown: unknown;
184
+ * try {
185
+ * asRobloxAssetId("12345abc");
186
+ * } catch (error) {
187
+ * thrown = error;
188
+ * }
189
+ *
190
+ * expect(id).toBe("12345");
191
+ * expect(thrown).toBeInstanceOf(RangeError);
192
+ * ```
193
+ *
194
+ * @param raw - Raw string to validate and brand.
195
+ * @returns The input re-typed as a {@link RobloxAssetId}.
196
+ * @throws RangeError when `raw` is empty or contains non-digit characters.
197
+ */
198
+ declare function asRobloxAssetId(raw: string): RobloxAssetId;
199
+ /**
200
+ * Type predicate: test whether a raw string is a valid {@link Sha256Hex}.
201
+ *
202
+ * Accepts exactly 64 lowercase hexadecimal characters (matching
203
+ * `/^[0-9a-f]{64}$/`). Prefer this when the caller owns error handling;
204
+ * use {@link asSha256Hex} when throwing is the right failure mode.
205
+ *
206
+ * @example
207
+ *
208
+ * ```ts
209
+ * import { isSha256Hex } from "@bedrock-rbx/core";
210
+ *
211
+ * const digest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
212
+ * const valid = isSha256Hex(digest);
213
+ * const invalid = isSha256Hex(digest.toUpperCase());
214
+ * expect(valid).toBe(true);
215
+ * expect(invalid).toBe(false);
216
+ * ```
217
+ *
218
+ * @param raw - String to test.
219
+ * @returns `true` when `raw` matches the Sha256Hex shape; narrows `raw`.
220
+ */
221
+ declare function isSha256Hex(raw: string): raw is Sha256Hex;
222
+ /**
223
+ * Validate and brand a raw string as a {@link Sha256Hex}.
224
+ *
225
+ * Accepts exactly 64 lowercase hexadecimal characters. Uppercase hex, lengths
226
+ * other than 64, and any non-hex character are rejected so the brand is a
227
+ * canonical representation.
228
+ *
229
+ * @example
230
+ *
231
+ * ```ts
232
+ * import { asSha256Hex } from "@bedrock-rbx/core";
233
+ *
234
+ * const digest = asSha256Hex(
235
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
236
+ * );
237
+ *
238
+ * let thrown: unknown;
239
+ * try {
240
+ * asSha256Hex("deadbeef");
241
+ * } catch (error) {
242
+ * thrown = error;
243
+ * }
244
+ *
245
+ * expect(digest).toHaveLength(64);
246
+ * expect(thrown).toBeInstanceOf(RangeError);
247
+ * ```
248
+ *
249
+ * @param raw - Raw string to validate and brand.
250
+ * @returns The input re-typed as a {@link Sha256Hex}.
251
+ * @throws RangeError when `raw` is not exactly 64 lowercase hex characters.
252
+ */
253
+ declare function asSha256Hex(raw: string): Sha256Hex;
254
+ //#endregion
255
+ //#region src/core/resources.d.ts
256
+ /**
257
+ * Desired state for a game pass, as declared in user config.
258
+ *
259
+ * Each field is `readonly` because desired state is treated as an immutable
260
+ * snapshot once normalized by `buildDesired`; downstream consumers (`diff`,
261
+ * drivers) must not mutate it.
262
+ *
263
+ * @example
264
+ *
265
+ * ```ts
266
+ * import { asResourceKey, asSha256Hex, type GamePassDesiredState } from "@bedrock-rbx/core";
267
+ *
268
+ * const pass: GamePassDesiredState = {
269
+ * description: "Grants VIP perks.",
270
+ * icon: { "en-us": "assets/vip-icon.png" },
271
+ * iconFileHashes: {
272
+ * "en-us": asSha256Hex(
273
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
274
+ * ),
275
+ * },
276
+ * key: asResourceKey("vip-pass"),
277
+ * kind: "gamePass",
278
+ * name: "VIP Pass",
279
+ * price: undefined,
280
+ * };
281
+ *
282
+ * expect(pass.kind).toBe("gamePass");
283
+ * expect(pass.price).toBeUndefined();
284
+ * ```
285
+ */
286
+ interface GamePassDesiredState {
287
+ /** User-supplied key; stable across deploys; used to correlate desired with current. */
288
+ readonly key: ResourceKey;
289
+ /** User-facing game-pass name as shown on the Roblox storefront. */
290
+ readonly name: string;
291
+ /** User-facing description shown on the game-pass detail page. */
292
+ readonly description: string;
293
+ /**
294
+ * Locale-keyed icon paths declared on the authored config. The Roblox
295
+ * game-pass API is monolingual, so only the `"en-us"` icon is ever
296
+ * uploaded; the map shape mirrors `UniverseDesiredState.icon` for
297
+ * cross-kind parity.
298
+ */
299
+ readonly icon: Record<"en-us", string>;
300
+ /**
301
+ * SHA-256 digests of the local icon files keyed by the same locales as
302
+ * the icon map. The diff compares this map against the prior current
303
+ * state so the driver re-uploads only when a file's bytes change.
304
+ */
305
+ readonly iconFileHashes: Record<"en-us", Sha256Hex>;
306
+ /** Discriminator tag for the `ResourceDesiredState` union. */
307
+ readonly kind: "gamePass";
308
+ /**
309
+ * Robux price. `undefined` means off-sale (mirrors Mantle's `Option<u32>`;
310
+ * the state parser normalizes JSON `null` to `undefined` at the wire
311
+ * boundary per the project type convention).
312
+ */
313
+ readonly price: number | undefined;
314
+ }
315
+ /**
316
+ * Desired state for a place, the `.rbxl` or `.rbxlx` file a universe serves
317
+ * as one of its levels.
318
+ *
319
+ * `placeId` sits on desired state (rather than on outputs like
320
+ * {@link GamePassOutputs.assetId}) because Roblox Open Cloud cannot mint
321
+ * places; the user supplies the existing place ID per entry. `filePath` and
322
+ * `fileHash` describe the local file the driver publishes; `buildDesired`
323
+ * computes `fileHash` from the file bytes so `diff` can detect drift without
324
+ * re-uploading unchanged content. `displayName`, `description`, and
325
+ * `serverSize` are optional metadata fields routed through
326
+ * `PlacesClient.update`; `undefined` leaves the server value untouched.
327
+ *
328
+ * @example
329
+ *
330
+ * ```ts
331
+ * import {
332
+ * asResourceKey,
333
+ * asRobloxAssetId,
334
+ * asSha256Hex,
335
+ * type PlaceDesiredState,
336
+ * } from "@bedrock-rbx/core";
337
+ *
338
+ * const place: PlaceDesiredState = {
339
+ * description: undefined,
340
+ * displayName: "Start Place",
341
+ * fileHash: asSha256Hex(
342
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
343
+ * ),
344
+ * filePath: "places/start.rbxl",
345
+ * key: asResourceKey("start-place"),
346
+ * kind: "place",
347
+ * placeId: asRobloxAssetId("4711"),
348
+ * serverSize: 50,
349
+ * };
350
+ *
351
+ * expect(place.kind).toBe("place");
352
+ * expect(place.displayName).toBe("Start Place");
353
+ * expect(place.description).toBeUndefined();
354
+ * expect(place.serverSize).toBe(50);
355
+ * ```
356
+ */
357
+ interface PlaceDesiredState {
358
+ /** User-supplied key; stable across deploys; used to correlate desired with current. */
359
+ readonly key: ResourceKey;
360
+ /** User-facing place description; `undefined` leaves the server value untouched. */
361
+ readonly description: string | undefined;
362
+ /** User-facing place name; `undefined` leaves the server value untouched. */
363
+ readonly displayName: string | undefined;
364
+ /** SHA-256 hex digest of the place file, computed by `buildDesired` in shell. */
365
+ readonly fileHash: Sha256Hex;
366
+ /** Path to the `.rbxl` or `.rbxlx` file on disk, relative to the config file. */
367
+ readonly filePath: string;
368
+ /** Discriminator tag for the `ResourceDesiredState` union. */
369
+ readonly kind: "place";
370
+ /** Existing Roblox place ID; Open Cloud cannot create places, so this is an input, not an output. */
371
+ readonly placeId: RobloxAssetId;
372
+ /** Maximum players per server; positive integer. `undefined` leaves the server value untouched. */
373
+ readonly serverSize: number | undefined;
374
+ }
375
+ /**
376
+ * Roblox-returned value produced by publishing a place version. The publish
377
+ * endpoint does not return an asset ID (the `placeId` is supplied by the
378
+ * caller); `versionNumber` is the only Roblox-assigned field the response
379
+ * carries.
380
+ */
381
+ interface PlaceOutputs {
382
+ /** Auto-incrementing version number assigned by Roblox on every publish. */
383
+ readonly versionNumber: number;
384
+ }
385
+ /**
386
+ * Desired state for the singleton universe a config manages.
387
+ *
388
+ * The universe is adopted rather than provisioned: the user supplies an
389
+ * existing `universeId` (Open Cloud cannot mint universes) and bedrock
390
+ * reconciles the declared managed fields against it.
391
+ *
392
+ * Most managed fields use `T | undefined` to mean "unmanaged": the diff
393
+ * treats `undefined` as absent and the driver omits the field from the
394
+ * `updateMask`. The clearable fields (`privateServerPriceRobux` and each
395
+ * social link) are additionally key-presence aware: a present key with
396
+ * `undefined` tells the driver to clear the server value (ocale emits
397
+ * JSON `null`), while an absent key leaves the server value untouched.
398
+ *
399
+ * @example
400
+ *
401
+ * ```ts
402
+ * import {
403
+ * asRobloxAssetId,
404
+ * UNIVERSE_SINGLETON_KEY,
405
+ * type UniverseDesiredState,
406
+ * } from "@bedrock-rbx/core";
407
+ *
408
+ * const universe: UniverseDesiredState = {
409
+ * consoleEnabled: undefined,
410
+ * desktopEnabled: true,
411
+ * discordSocialLink: { title: "Join our Discord", uri: "https://discord.gg/example" },
412
+ * displayName: "Fun Universe",
413
+ * key: UNIVERSE_SINGLETON_KEY,
414
+ * kind: "universe",
415
+ * mobileEnabled: false,
416
+ * privateServerPriceRobux: undefined,
417
+ * tabletEnabled: undefined,
418
+ * twitterSocialLink: undefined,
419
+ * universeId: asRobloxAssetId("1234567890"),
420
+ * voiceChatEnabled: true,
421
+ * vrEnabled: undefined,
422
+ * };
423
+ *
424
+ * expect(universe.kind).toBe("universe");
425
+ * expect("privateServerPriceRobux" in universe).toBeTrue();
426
+ * expect(universe.discordSocialLink?.title).toBe("Join our Discord");
427
+ * expect(universe.twitterSocialLink).toBeUndefined();
428
+ * ```
429
+ */
430
+ interface UniverseDesiredState {
431
+ /** Fixed singleton key (`"main"`); bedrock synthesizes it in `flattenConfig`. */
432
+ readonly key: ResourceKey;
433
+ /** Whether console players can join; `undefined` leaves the server value untouched. */
434
+ readonly consoleEnabled: boolean | undefined;
435
+ /** Whether desktop players can join; `undefined` leaves the server value untouched. */
436
+ readonly desktopEnabled: boolean | undefined;
437
+ /** Discord social link; tri-state (absent/undefined/set) — see interface JSDoc. */
438
+ readonly discordSocialLink?: SocialLink$1 | undefined;
439
+ /**
440
+ * Display name for the universe. `undefined` leaves the server
441
+ * value untouched. The driver routes declared updates through
442
+ * `PlacesClient.update` because the universe PATCH endpoint treats
443
+ * `displayName` as read-only.
444
+ */
445
+ readonly displayName: string | undefined;
446
+ /** Facebook social link; tri-state (absent/undefined/set) — see interface JSDoc. */
447
+ readonly facebookSocialLink?: SocialLink$1 | undefined;
448
+ /** Guilded social link; tri-state (absent/undefined/set) — see interface JSDoc. */
449
+ readonly guildedSocialLink?: SocialLink$1 | undefined;
450
+ /**
451
+ * Locale-keyed experience-icon paths declared on the authored config.
452
+ * Absent when the user did not declare an icon block.
453
+ */
454
+ readonly icon?: Record<"en-us", string>;
455
+ /**
456
+ * SHA-256 digests of the local icon files keyed by the same locales as
457
+ * the icon map. The diff compares this map against the prior current
458
+ * state so the driver re-uploads only when a file's bytes change.
459
+ */
460
+ readonly iconFileHashes?: Record<"en-us", Sha256Hex>;
461
+ /** Discriminator tag for the `ResourceDesiredState` union. */
462
+ readonly kind: "universe";
463
+ /** Whether mobile players can join; `undefined` leaves the server value untouched. */
464
+ readonly mobileEnabled: boolean | undefined;
465
+ /**
466
+ * Private-server price in Robux. A present key with `undefined`
467
+ * clears the server value on apply; an absent key leaves the server
468
+ * value untouched.
469
+ */
470
+ readonly privateServerPriceRobux?: number | undefined;
471
+ /** Roblox Group social link; tri-state (absent/undefined/set) — see interface JSDoc. */
472
+ readonly robloxGroupSocialLink?: SocialLink$1 | undefined;
473
+ /** Whether tablet players can join; `undefined` leaves the server value untouched. */
474
+ readonly tabletEnabled: boolean | undefined;
475
+ /** Twitch social link; tri-state (absent/undefined/set) — see interface JSDoc. */
476
+ readonly twitchSocialLink?: SocialLink$1 | undefined;
477
+ /** Twitter social link; tri-state (absent/undefined/set) — see interface JSDoc. */
478
+ readonly twitterSocialLink?: SocialLink$1 | undefined;
479
+ /** User-supplied Roblox universe ID; the universe must already exist. */
480
+ readonly universeId: RobloxAssetId;
481
+ /** Whether voice chat is enabled; `undefined` leaves the server value untouched. */
482
+ readonly voiceChatEnabled: boolean | undefined;
483
+ /** Whether VR players can join; `undefined` leaves the server value untouched. */
484
+ readonly vrEnabled: boolean | undefined;
485
+ /** YouTube social link; tri-state (absent/undefined/set) — see interface JSDoc. */
486
+ readonly youtubeSocialLink?: SocialLink$1 | undefined;
487
+ }
488
+ /**
489
+ * Tuple of every social link field name on {@link UniverseDesiredState}.
490
+ * Iterated by flatten, driver, and diff to handle the tri-state clearable
491
+ * semantics uniformly across all seven fields.
492
+ */
493
+ declare const SOCIAL_LINK_FIELDS: readonly ["discordSocialLink", "facebookSocialLink", "guildedSocialLink", "robloxGroupSocialLink", "twitchSocialLink", "twitterSocialLink", "youtubeSocialLink"];
494
+ /** Union of the seven social link field names on {@link UniverseDesiredState}. */
495
+ type SocialLinkField = (typeof SOCIAL_LINK_FIELDS)[number];
496
+ /**
497
+ * Desired state for a developer product, the consumable a player can buy via
498
+ * `MarketplaceService:PromptProductPurchase`.
499
+ *
500
+ * @example
501
+ *
502
+ * ```ts
503
+ * import { asResourceKey, type DeveloperProductDesiredState } from "@bedrock-rbx/core";
504
+ *
505
+ * const product: DeveloperProductDesiredState = {
506
+ * description: "Stocks the player up with 1,000 premium gems.",
507
+ * isRegionalPricingEnabled: true,
508
+ * key: asResourceKey("gem-pack"),
509
+ * kind: "developerProduct",
510
+ * name: "Gem Pack",
511
+ * price: 100,
512
+ * storePageEnabled: true,
513
+ * };
514
+ *
515
+ * expect(product.kind).toBe("developerProduct");
516
+ * expect(product.price).toBe(100);
517
+ * ```
518
+ */
519
+ interface DeveloperProductDesiredState {
520
+ /** User-supplied key; stable across deploys; used to correlate desired with current. */
521
+ readonly key: ResourceKey;
522
+ /** User-facing developer product name as shown on the storefront. */
523
+ readonly name: string;
524
+ /** User-facing description shown on the developer product detail page. */
525
+ readonly description: string;
526
+ /**
527
+ * Locale-keyed icon paths declared on the authored config. Absent when
528
+ * the user did not declare an icon block. The Roblox developer-product
529
+ * API is monolingual, so only the `"en-us"` icon is ever uploaded; the
530
+ * map shape mirrors `GamePassDesiredState.icon` for cross-kind parity.
531
+ */
532
+ readonly icon?: Record<"en-us", string>;
533
+ /**
534
+ * SHA-256 digests of the local icon files keyed by the same locales as
535
+ * the icon map. The diff compares this map against the prior current
536
+ * state so the driver re-uploads only when a file's bytes change. Absent
537
+ * when `icon` is absent.
538
+ */
539
+ readonly iconFileHashes?: Record<"en-us", Sha256Hex>;
540
+ /**
541
+ * Whether Roblox-managed regional pricing applies to the product.
542
+ * Tri-state: `undefined` means the flag is unmanaged (the diff ignores
543
+ * it); a defined value is propagated to Roblox on every deploy.
544
+ */
545
+ readonly isRegionalPricingEnabled: boolean | undefined;
546
+ /** Discriminator tag for the `ResourceDesiredState` union. */
547
+ readonly kind: "developerProduct";
548
+ /**
549
+ * Robux price. `undefined` means off-sale; removing the field from config
550
+ * takes the product off-sale on the next deploy, re-adding puts it back
551
+ * on sale.
552
+ */
553
+ readonly price: number | undefined;
554
+ /**
555
+ * Whether the product appears on the universe's external store page.
556
+ * Tri-state: `undefined` means the flag is unmanaged. A defined value is
557
+ * applied via a follow-up PATCH after the create POST because the v2
558
+ * create endpoint does not accept this field.
559
+ */
560
+ readonly storePageEnabled: boolean | undefined;
561
+ }
562
+ /**
563
+ * Discriminated union of every desired-state shape Bedrock manages.
564
+ *
565
+ * Extend by adding new members to this union; the mapped
566
+ * `ResourceOutputsByKind` interface then forces a matching outputs entry for
567
+ * the new kind at compile time.
568
+ */
569
+ type ResourceDesiredState = DeveloperProductDesiredState | GamePassDesiredState | PlaceDesiredState | UniverseDesiredState;
570
+ /**
571
+ * Roblox-returned identifiers produced by creating or updating a game pass.
572
+ *
573
+ * Distinct from `GamePassDesiredState.key`: the desired-state key is a
574
+ * user-supplied handle, while these IDs are assigned by Roblox and
575
+ * discovered only after the first successful API call.
576
+ */
577
+ interface GamePassOutputs {
578
+ /** Primary Roblox asset ID for the game pass itself. */
579
+ readonly assetId: RobloxAssetId;
580
+ /**
581
+ * Locale-keyed Roblox-assigned image IDs for the game-pass icons.
582
+ * Mirrors `UniverseOutputs.iconAssetIds` for cross-kind shape parity;
583
+ * the Roblox game-pass API only ever populates the `"en-us"` entry.
584
+ */
585
+ readonly iconAssetIds: Record<"en-us", RobloxAssetId>;
586
+ }
587
+ /**
588
+ * Roblox-returned identifiers produced by creating or updating a developer
589
+ * product. `productId` is what `MarketplaceService:PromptProductPurchase`
590
+ * accepts and is stable across re-deploys. `iconImageAssetId` is optional
591
+ * because the wire returns it as nullable when no icon is uploaded; slice 1
592
+ * never populates it because icon support lands in a later slice.
593
+ */
594
+ interface DeveloperProductOutputs {
595
+ /** Roblox asset ID of the uploaded icon image; `undefined` when no icon is uploaded. */
596
+ readonly iconImageAssetId?: RobloxAssetId | undefined;
597
+ /** Roblox-assigned developer product ID; stable across re-deploys. */
598
+ readonly productId: RobloxAssetId;
599
+ }
600
+ /**
601
+ * Roblox-returned value produced by reconciling a universe. The root place
602
+ * ID is server-authoritative: bedrock cannot set it directly, but records it
603
+ * so a future places slice can cross-validate the declared start place.
604
+ */
605
+ interface UniverseOutputs {
606
+ /**
607
+ * Locale-keyed Roblox-assigned image IDs for the experience icons. Only
608
+ * populated for locales whose icon was uploaded by the universe driver;
609
+ * the entry persists across re-deploys until the locale is removed from
610
+ * the authored config.
611
+ */
612
+ readonly iconAssetIds?: Record<"en-us", RobloxAssetId>;
613
+ /** Server-assigned root place ID for the universe. */
614
+ readonly rootPlaceId: RobloxAssetId;
615
+ }
616
+ /**
617
+ * String union of every discriminator tag in `ResourceDesiredState`.
618
+ *
619
+ * Derived from the union rather than hand-maintained so adding a new
620
+ * `ResourceDesiredState` member automatically widens this type.
621
+ */
622
+ type ResourceKind = ResourceDesiredState["kind"];
623
+ /**
624
+ * Per-kind outputs registry. Each `ResourceKind` must have a matching entry
625
+ * or `ResourceOutputs<K>` is a compile error. Modelled as an interface (not a
626
+ * type alias) so downstream packages can use declaration merging to register
627
+ * outputs for new kinds without touching this module.
628
+ *
629
+ * @example
630
+ *
631
+ * ```ts
632
+ * import { asRobloxAssetId, type ResourceOutputsByKind } from "@bedrock-rbx/core";
633
+ *
634
+ * const outputs: ResourceOutputsByKind["gamePass"] = {
635
+ * assetId: asRobloxAssetId("9876543210"),
636
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
637
+ * };
638
+ *
639
+ * expect(outputs.assetId).toBe("9876543210");
640
+ * ```
641
+ */
642
+ interface ResourceOutputsByKind {
643
+ /** Outputs returned by the Roblox API for a developer-product resource. */
644
+ developerProduct: DeveloperProductOutputs;
645
+ /** Outputs returned by the Roblox API for a game-pass resource. */
646
+ gamePass: GamePassOutputs;
647
+ /** Outputs returned by the Roblox API for a place publish. */
648
+ place: PlaceOutputs;
649
+ /** Outputs returned by the Roblox API for a universe reconcile. */
650
+ universe: UniverseOutputs;
651
+ }
652
+ /**
653
+ * Resolved outputs for a specific resource kind.
654
+ *
655
+ * @template K - The resource kind discriminator.
656
+ */
657
+ type ResourceOutputs<K extends ResourceKind> = ResourceOutputsByKind[K];
658
+ /**
659
+ * Current (live) state for a resource kind.
660
+ *
661
+ * Composed from the matching desired-state shape plus a nested `outputs`
662
+ * object carrying Roblox-assigned identifiers. The outer `K extends
663
+ * ResourceKind` conditional distributes `K` across the union so the default
664
+ * `ResourceCurrentState` resolves to a clean per-kind union rather than a
665
+ * cross-product intersection of every kind's fields.
666
+ *
667
+ * The `outputs` sub-object stays nested (rather than flattening into the
668
+ * top level) to mirror Mantle's `{ inputs, outputs }` state layout,
669
+ * keeping migration copy clean.
670
+ *
671
+ * @template K - The resource kind discriminator. Defaults to the full
672
+ * `ResourceKind` union for the broad form used in `ReadonlyArray`
673
+ * collections.
674
+ *
675
+ * @example
676
+ *
677
+ * ```ts
678
+ * import {
679
+ * asResourceKey,
680
+ * asRobloxAssetId,
681
+ * asSha256Hex,
682
+ * type ResourceCurrentState,
683
+ * } from "@bedrock-rbx/core";
684
+ *
685
+ * const current: ResourceCurrentState<"gamePass"> = {
686
+ * description: "Grants VIP perks.",
687
+ * icon: { "en-us": "assets/vip-icon.png" },
688
+ * iconFileHashes: {
689
+ * "en-us": asSha256Hex(
690
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
691
+ * ),
692
+ * },
693
+ * key: asResourceKey("vip-pass"),
694
+ * kind: "gamePass",
695
+ * name: "VIP Pass",
696
+ * outputs: {
697
+ * assetId: asRobloxAssetId("9876543210"),
698
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
699
+ * },
700
+ * price: 500,
701
+ * };
702
+ *
703
+ * expect(current.outputs.assetId).toBe("9876543210");
704
+ * expect(current.kind).toBe("gamePass");
705
+ * ```
706
+ */
707
+ type ResourceCurrentState<K extends ResourceKind = ResourceKind> = K extends ResourceKind ? Prettify<Extract<ResourceDesiredState, {
708
+ kind: K;
709
+ }> & {
710
+ readonly outputs: ResourceOutputs<K>;
711
+ }> : never;
712
+ type Prettify<T> = { readonly [K in keyof T]: T[K] };
713
+ /**
714
+ * Fixed stable key for the singleton universe resource. `flattenConfig`
715
+ * stamps this onto the sole `UniverseDesiredInput` it emits; fixtures and
716
+ * state adapters share the constant so the invariant is encoded once.
717
+ *
718
+ * @example
719
+ *
720
+ * ```ts
721
+ * import { UNIVERSE_SINGLETON_KEY } from "@bedrock-rbx/core";
722
+ *
723
+ * expect(UNIVERSE_SINGLETON_KEY).toBe("main");
724
+ * ```
725
+ */
726
+ declare const UNIVERSE_SINGLETON_KEY: ResourceKey;
727
+ //#endregion
728
+ //#region src/ports/resource-driver.d.ts
729
+ /**
730
+ * Plugin contract for a resource adapter: the interface a third-party author
731
+ * implements to teach Bedrock how to reconcile one {@link ResourceKind} against
732
+ * its upstream API.
733
+ *
734
+ * `ResourceDriver<K>` is a *driven* (secondary) port in hexagonal terms; the
735
+ * name "driver" follows Terraform, Pulumi, and Mantle IaC convention for a
736
+ * component that talks to a specific resource API.
737
+ *
738
+ * @template K - The {@link ResourceKind} discriminator this driver handles.
739
+ *
740
+ * @example
741
+ *
742
+ * ```ts
743
+ * import {
744
+ * asResourceKey,
745
+ * asRobloxAssetId,
746
+ * asSha256Hex,
747
+ * type ResourceDriver,
748
+ * } from "@bedrock-rbx/core";
749
+ *
750
+ * const gamePassDriver: ResourceDriver<"gamePass"> = {
751
+ * async create(desired) {
752
+ * return {
753
+ * data: {
754
+ * ...desired,
755
+ * outputs: {
756
+ * assetId: asRobloxAssetId("9876543210"),
757
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
758
+ * },
759
+ * },
760
+ * success: true,
761
+ * };
762
+ * },
763
+ * };
764
+ *
765
+ * return gamePassDriver
766
+ * .create({
767
+ * description: "Grants VIP perks.",
768
+ * icon: { "en-us": "assets/vip-icon.png" },
769
+ * iconFileHashes: {
770
+ * "en-us": asSha256Hex(
771
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
772
+ * ),
773
+ * },
774
+ * key: asResourceKey("vip-pass"),
775
+ * kind: "gamePass",
776
+ * name: "VIP Pass",
777
+ * price: undefined,
778
+ * })
779
+ * .then((result) => {
780
+ * expect(result.success).toBeTrue();
781
+ * if (result.success) {
782
+ * expect(result.data.outputs.assetId).toBe("9876543210");
783
+ * }
784
+ * });
785
+ * ```
786
+ */
787
+ interface ResourceDriver<K extends ResourceKind> {
788
+ /**
789
+ * Create the resource upstream from its desired state and return the
790
+ * resulting current state (desired fields + Roblox-assigned outputs).
791
+ */
792
+ create(desired: Extract<ResourceDesiredState, {
793
+ kind: K;
794
+ }>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
795
+ /**
796
+ * Reconcile an upstream resource whose managed content has drifted from its
797
+ * desired state. Receives the last-known current state so the driver can
798
+ * compute a minimal patch (or no-op upstream, for file-backed kinds where
799
+ * republishing is unconditional).
800
+ *
801
+ * Optional. Drivers whose upstream API has no update operation omit this
802
+ * method; `applyOps` surfaces an `updateUnsupported` error at dispatch time
803
+ * instead.
804
+ */
805
+ update?(current: ResourceCurrentState<K>, desired: Extract<ResourceDesiredState, {
806
+ kind: K;
807
+ }>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
808
+ }
809
+ /**
810
+ * Polymorphic dispatch table keyed by {@link ResourceKind}, mapping each kind
811
+ * to the {@link ResourceDriver} that handles it. `applyOps` indexes the
812
+ * registry by `op.desired.kind` to reach the matching driver with full type
813
+ * safety: adding a new kind to `ResourceDesiredState` is a compile error until
814
+ * a matching registry entry is supplied.
815
+ *
816
+ * @example
817
+ *
818
+ * ```ts
819
+ * import { OpenCloudError, type DriverRegistry } from "@bedrock-rbx/core";
820
+ *
821
+ * const registry: DriverRegistry = {
822
+ * gamePass: {
823
+ * async create() {
824
+ * return { err: new OpenCloudError("not implemented"), success: false };
825
+ * },
826
+ * },
827
+ * place: {
828
+ * async create() {
829
+ * return { err: new OpenCloudError("not implemented"), success: false };
830
+ * },
831
+ * },
832
+ * universe: {
833
+ * async create() {
834
+ * return { err: new OpenCloudError("not implemented"), success: false };
835
+ * },
836
+ * },
837
+ * developerProduct: {
838
+ * async create() {
839
+ * return { err: new OpenCloudError("not implemented"), success: false };
840
+ * },
841
+ * },
842
+ * };
843
+ *
844
+ * expect(registry.gamePass).toBeObject();
845
+ * ```
846
+ */
847
+ type DriverRegistry = { [K in ResourceKind]: ResourceDriver<K> };
848
+ //#endregion
849
+ //#region src/adapters/developer-product-driver.d.ts
850
+ /**
851
+ * Dependencies of `createDeveloperProductDriver`. `universeId` is captured
852
+ * at construction time (matching `GamePassDriverDeps`) so each driver
853
+ * instance is bound to a single universe; multi-universe deploys construct
854
+ * one driver per universe. `readFile` exists on the driver (not upstream
855
+ * in shell) because icon hashes flow through `diff` but bytes do not.
856
+ *
857
+ * @example
858
+ *
859
+ * ```ts
860
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
861
+ * import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
862
+ * import { asRobloxAssetId, type DeveloperProductDriverDeps } from "@bedrock-rbx/core";
863
+ *
864
+ * const httpClient: HttpClient = {
865
+ * async request() {
866
+ * return { data: { body: {}, headers: {}, status: 200 }, success: true };
867
+ * },
868
+ * };
869
+ *
870
+ * const deps: DeveloperProductDriverDeps = {
871
+ * client: new DeveloperProductsClient({
872
+ * apiKey: "rbx-your-key",
873
+ * httpClient,
874
+ * sleep: async () => {},
875
+ * }),
876
+ * readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
877
+ * universeId: asRobloxAssetId("1234567890"),
878
+ * };
879
+ *
880
+ * expect(deps.universeId).toBe("1234567890");
881
+ * ```
882
+ */
883
+ interface DeveloperProductDriverDeps {
884
+ /** Configured developer-products client from `@bedrock-rbx/ocale/developer-products`. */
885
+ readonly client: DeveloperProductsClient;
886
+ /** Reads icon bytes for upload; rejections propagate out of `create` and `update`. */
887
+ readonly readFile: (path: string) => Promise<Uint8Array>;
888
+ /** Universe that owns every developer product this driver creates. */
889
+ readonly universeId: RobloxAssetId;
890
+ }
891
+ /**
892
+ * Wraps {@link DeveloperProductsClient} as a `ResourceDriver<"developerProduct">`
893
+ * that maps a desired-state entry to an ocale create or update call and the
894
+ * response back to a `ResourceCurrentState<"developerProduct">`. The
895
+ * `update` path consumes the upstream `204 No Content` response and
896
+ * synthesizes the post-update `ResourceCurrentState` from `desired` plus
897
+ * the existing `current.outputs`, carrying `iconImageAssetId` forward when
898
+ * present.
899
+ *
900
+ * Upstream `OpenCloudError` results pass through as `Result` failures.
901
+ *
902
+ * @param deps - Injected ocale client and owning universe.
903
+ * @returns A driver indexable by `"developerProduct"` in a `DriverRegistry`.
904
+ *
905
+ * @example
906
+ *
907
+ * ```ts
908
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
909
+ * import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
910
+ * import {
911
+ * asResourceKey,
912
+ * asRobloxAssetId,
913
+ * createDeveloperProductDriver,
914
+ * } from "@bedrock-rbx/core";
915
+ *
916
+ * const httpClient: HttpClient = {
917
+ * async request() {
918
+ * return {
919
+ * data: {
920
+ * body: {
921
+ * createdTimestamp: "2024-01-15T10:30:00.000Z",
922
+ * description: "Stocks the player up with 1,000 premium gems.",
923
+ * iconImageAssetId: null,
924
+ * isForSale: false,
925
+ * isImmutable: false,
926
+ * name: "Gem Pack",
927
+ * priceInformation: null,
928
+ * productId: 9_876_543_210,
929
+ * storePageEnabled: false,
930
+ * universeId: 1_234_567_890,
931
+ * updatedTimestamp: "2024-01-15T10:30:00.000Z",
932
+ * },
933
+ * headers: {},
934
+ * status: 200,
935
+ * },
936
+ * success: true,
937
+ * };
938
+ * },
939
+ * };
940
+ *
941
+ * const driver = createDeveloperProductDriver({
942
+ * client: new DeveloperProductsClient({
943
+ * apiKey: "rbx-your-key",
944
+ * httpClient,
945
+ * sleep: async () => {},
946
+ * }),
947
+ * readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
948
+ * universeId: asRobloxAssetId("1234567890"),
949
+ * });
950
+ *
951
+ * return driver
952
+ * .create({
953
+ * description: "Stocks the player up with 1,000 premium gems.",
954
+ * isRegionalPricingEnabled: undefined,
955
+ * key: asResourceKey("gem-pack"),
956
+ * kind: "developerProduct",
957
+ * name: "Gem Pack",
958
+ * price: undefined,
959
+ * storePageEnabled: undefined,
960
+ * })
961
+ * .then((result) => {
962
+ * expect(result.success).toBeTrue();
963
+ * if (result.success) {
964
+ * expect(result.data.outputs.productId).toBe("9876543210");
965
+ * }
966
+ * });
967
+ * ```
968
+ */
969
+ declare function createDeveloperProductDriver(deps: DeveloperProductDriverDeps): ResourceDriver<"developerProduct">;
970
+ //#endregion
971
+ //#region src/adapters/game-pass-driver.d.ts
972
+ /**
973
+ * `universeId` is captured at construction time rather than on
974
+ * `GamePassDesiredState` so state files round-trip with Mantle's `PassInputs`
975
+ * shape. `readFile` exists on the driver (not upstream in shell) because icon
976
+ * hashes flow through `diff` but bytes do not.
977
+ *
978
+ * @example
979
+ *
980
+ * ```ts
981
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
982
+ * import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
983
+ * import { asRobloxAssetId, type GamePassDriverDeps } from "@bedrock-rbx/core";
984
+ *
985
+ * const httpClient: HttpClient = {
986
+ * async request() {
987
+ * return { data: { body: {}, headers: {}, status: 200 }, success: true };
988
+ * },
989
+ * };
990
+ *
991
+ * const deps: GamePassDriverDeps = {
992
+ * client: new GamePassesClient({
993
+ * apiKey: "rbx-your-key",
994
+ * httpClient,
995
+ * sleep: async () => {},
996
+ * }),
997
+ * readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
998
+ * universeId: asRobloxAssetId("1234567890"),
999
+ * };
1000
+ *
1001
+ * expect(deps.universeId).toBe("1234567890");
1002
+ * ```
1003
+ */
1004
+ interface GamePassDriverDeps {
1005
+ /** Configured game-passes client from `@bedrock-rbx/ocale/game-passes`. */
1006
+ readonly client: GamePassesClient;
1007
+ /** Reads icon bytes for upload; rejections propagate out of `create`. */
1008
+ readonly readFile: (path: string) => Promise<Uint8Array>;
1009
+ /** Universe that owns every game pass this driver creates. */
1010
+ readonly universeId: RobloxAssetId;
1011
+ }
1012
+ /**
1013
+ * Wraps {@link GamePassesClient} as a `ResourceDriver<"gamePass">` that maps
1014
+ * a desired-state entry to an ocale create call and the response back to a
1015
+ * `ResourceCurrentState<"gamePass">`.
1016
+ *
1017
+ * Upstream `OpenCloudError` results pass through as `Result` failures.
1018
+ * Filesystem errors from `deps.readFile` do not fit the `OpenCloudError`
1019
+ * shape and propagate as promise rejections; shell callers are expected to
1020
+ * translate them if a unified error surface is required.
1021
+ *
1022
+ * @param deps - Injected ocale client, file reader, and owning universe.
1023
+ * @returns A driver indexable by `"gamePass"` in a `DriverRegistry`.
1024
+ * @throws Whatever `deps.readFile` rejects with.
1025
+ *
1026
+ * @example
1027
+ *
1028
+ * ```ts
1029
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
1030
+ * import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
1031
+ * import {
1032
+ * asResourceKey,
1033
+ * asRobloxAssetId,
1034
+ * asSha256Hex,
1035
+ * createGamePassDriver,
1036
+ * } from "@bedrock-rbx/core";
1037
+ *
1038
+ * const httpClient: HttpClient = {
1039
+ * async request() {
1040
+ * return {
1041
+ * data: {
1042
+ * body: {
1043
+ * createdTimestamp: "2024-01-15T10:30:00.000Z",
1044
+ * description: "Grants VIP perks.",
1045
+ * gamePassId: 9_876_543_210,
1046
+ * iconAssetId: 1_122_334_455,
1047
+ * isForSale: true,
1048
+ * name: "VIP Pass",
1049
+ * updatedTimestamp: "2024-01-15T10:30:00.000Z",
1050
+ * },
1051
+ * headers: {},
1052
+ * status: 200,
1053
+ * },
1054
+ * success: true,
1055
+ * };
1056
+ * },
1057
+ * };
1058
+ *
1059
+ * const driver = createGamePassDriver({
1060
+ * client: new GamePassesClient({
1061
+ * apiKey: "rbx-your-key",
1062
+ * httpClient,
1063
+ * sleep: async () => {},
1064
+ * }),
1065
+ * readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
1066
+ * universeId: asRobloxAssetId("1234567890"),
1067
+ * });
1068
+ *
1069
+ * return driver
1070
+ * .create({
1071
+ * description: "Grants VIP perks.",
1072
+ * icon: { "en-us": "assets/vip-icon.png" },
1073
+ * iconFileHashes: {
1074
+ * "en-us": asSha256Hex(
1075
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1076
+ * ),
1077
+ * },
1078
+ * key: asResourceKey("vip-pass"),
1079
+ * kind: "gamePass",
1080
+ * name: "VIP Pass",
1081
+ * price: 500,
1082
+ * })
1083
+ * .then((result) => {
1084
+ * expect(result.success).toBeTrue();
1085
+ * if (result.success) {
1086
+ * expect(result.data.outputs.assetId).toBe("9876543210");
1087
+ * }
1088
+ * });
1089
+ * ```
1090
+ */
1091
+ declare function createGamePassDriver(deps: GamePassDriverDeps): ResourceDriver<"gamePass">;
1092
+ //#endregion
1093
+ //#region src/core/state.d.ts
1094
+ /**
1095
+ * In-memory state snapshot for one environment.
1096
+ *
1097
+ * The on-disk JSON wraps this shape with a `$bedrock: { version: N }` envelope.
1098
+ * Adapters flatten the envelope on read and re-wrap it on write; nothing
1099
+ * outside an adapter sees the `$bedrock` key.
1100
+ *
1101
+ * `version` is a literal so a breaking schema change is a compile-time type
1102
+ * shift rather than a silently accepted runtime value.
1103
+ *
1104
+ * @example
1105
+ *
1106
+ * ```ts
1107
+ * import {
1108
+ * asResourceKey,
1109
+ * asRobloxAssetId,
1110
+ * asSha256Hex,
1111
+ * type BedrockState,
1112
+ * } from "@bedrock-rbx/core";
1113
+ *
1114
+ * const state: BedrockState = {
1115
+ * environment: "production",
1116
+ * resources: [
1117
+ * {
1118
+ * description: "Grants VIP perks.",
1119
+ * icon: { "en-us": "assets/vip-icon.png" },
1120
+ * iconFileHashes: {
1121
+ * "en-us": asSha256Hex(
1122
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1123
+ * ),
1124
+ * },
1125
+ * key: asResourceKey("vip-pass"),
1126
+ * kind: "gamePass",
1127
+ * name: "VIP Pass",
1128
+ * outputs: {
1129
+ * assetId: asRobloxAssetId("9876543210"),
1130
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
1131
+ * },
1132
+ * price: 500,
1133
+ * },
1134
+ * ],
1135
+ * version: 1,
1136
+ * };
1137
+ *
1138
+ * expect(state.version).toBe(1);
1139
+ * expect(state.resources).toHaveLength(1);
1140
+ * ```
1141
+ */
1142
+ interface BedrockState {
1143
+ /** Environment name this snapshot belongs to (e.g. `"production"`, `"staging"`). */
1144
+ readonly environment: string;
1145
+ /** Current state of every resource Bedrock manages in this environment. */
1146
+ readonly resources: ReadonlyArray<ResourceCurrentState>;
1147
+ /** Schema-version literal; bumped only for breaking changes to the on-disk format. */
1148
+ readonly version: 1;
1149
+ }
1150
+ /**
1151
+ * Failure surfaced by a `StatePort` when a state file exists but cannot be
1152
+ * trusted: corrupt JSON, schema failure, or an unknown `$bedrock.version`.
1153
+ *
1154
+ * Narrow on `kind` rather than using `instanceof`: `StateError` is plain data,
1155
+ * not a thrown error subclass.
1156
+ *
1157
+ * @example
1158
+ *
1159
+ * ```ts
1160
+ * import type { StateError } from "@bedrock-rbx/core";
1161
+ *
1162
+ * const err: StateError = {
1163
+ * file: ".bedrock/state/production.json",
1164
+ * kind: "stateError",
1165
+ * reason: "Corrupt JSON: unexpected token at line 1 column 5",
1166
+ * };
1167
+ *
1168
+ * expect(err.kind).toBe("stateError");
1169
+ * ```
1170
+ */
1171
+ interface StateError {
1172
+ /** Adapter-specific path or identifier of the file that failed to parse. */
1173
+ readonly file: string;
1174
+ /** Literal discriminator for narrowing. */
1175
+ readonly kind: "stateError";
1176
+ /** Human-readable explanation of why the file could not be trusted. */
1177
+ readonly reason: string;
1178
+ }
1179
+ //#endregion
1180
+ //#region src/ports/state-port.d.ts
1181
+ /**
1182
+ * Plugin contract for persisting deployment state: the interface an adapter
1183
+ * (Gist, local filesystem, cloud object store) implements to let Bedrock load
1184
+ * and save its per-environment {@link BedrockState} snapshot.
1185
+ *
1186
+ * `StatePort` is a *driven* (secondary) port in hexagonal terms, following the
1187
+ * same naming convention as {@link "./resource-driver".ResourceDriver}.
1188
+ *
1189
+ * @example
1190
+ *
1191
+ * ```ts
1192
+ * import type { BedrockState, StatePort } from "@bedrock-rbx/core";
1193
+ *
1194
+ * const store = new Map<string, BedrockState>();
1195
+ *
1196
+ * const statePort: StatePort = {
1197
+ * async read(environment) {
1198
+ * return { data: store.get(environment), success: true };
1199
+ * },
1200
+ * async write(state) {
1201
+ * store.set(state.environment, state);
1202
+ * return { data: undefined, success: true };
1203
+ * },
1204
+ * };
1205
+ *
1206
+ * return statePort
1207
+ * .read("production")
1208
+ * .then((firstRead) => {
1209
+ * expect(firstRead.success).toBeTrue();
1210
+ * if (firstRead.success) {
1211
+ * expect(firstRead.data).toBeUndefined();
1212
+ * }
1213
+ * return statePort.write({
1214
+ * environment: "production",
1215
+ * resources: [],
1216
+ * version: 1,
1217
+ * });
1218
+ * })
1219
+ * .then((writeResult) => {
1220
+ * expect(writeResult.success).toBeTrue();
1221
+ * return statePort.read("production");
1222
+ * })
1223
+ * .then((secondRead) => {
1224
+ * expect(secondRead.success).toBeTrue();
1225
+ * if (secondRead.success && secondRead.data !== undefined) {
1226
+ * expect(secondRead.data.environment).toBe("production");
1227
+ * expect(secondRead.data.resources).toBeEmpty();
1228
+ * }
1229
+ * });
1230
+ * ```
1231
+ */
1232
+ interface StatePort {
1233
+ /**
1234
+ * Reads state for the given environment.
1235
+ *
1236
+ * - Returns `Ok(undefined)` when no state file exists (legitimate first deploy).
1237
+ * - Returns `Err(StateError)` when a file exists but cannot be parsed
1238
+ * (corrupt JSON, schema failure, unknown `$bedrock.version`).
1239
+ *
1240
+ * Never silently falls back to empty state: a malformed file that collapsed
1241
+ * to `{ resources: [] }` would cause the next apply to re-create every
1242
+ * resource on Roblox.
1243
+ */
1244
+ read(environment: string): Promise<Result$1<BedrockState | undefined, StateError>>;
1245
+ /** Writes state for the given environment, overwriting any existing file. */
1246
+ write(state: BedrockState): Promise<Result$1<void, StateError>>;
1247
+ }
1248
+ //#endregion
1249
+ //#region src/adapters/gist-state-adapter.d.ts
1250
+ /**
1251
+ * Minimal `fetch`-compatible signature the adapter needs, narrower than
1252
+ * `typeof globalThis.fetch` so test fakes do not have to stub runtime
1253
+ * extensions such as `fetch.preconnect`.
1254
+ */
1255
+ type GistFetch = (input: globalThis.Request | string | URL, init?: RequestInit) => Promise<Response>;
1256
+ /**
1257
+ * Configuration for {@link createGistStateAdapter}.
1258
+ */
1259
+ interface GistStateAdapterDeps {
1260
+ /** Injection seam for tests; defaults to `globalThis.fetch`. */
1261
+ readonly fetch?: GistFetch | undefined;
1262
+ /** ID of an existing GitHub Gist that holds this project's state files. */
1263
+ readonly gistId: string;
1264
+ /**
1265
+ * Injection seam for retry backoff timing; defaults to a `setTimeout`-based
1266
+ * promise. Tests pass a fake to keep retry assertions deterministic.
1267
+ */
1268
+ readonly sleep?: ((ms: number) => Promise<void>) | undefined;
1269
+ /** GitHub token (fine-grained PAT or classic PAT) with gist read/write scope. */
1270
+ readonly token: string;
1271
+ }
1272
+ /**
1273
+ * Build a `StatePort` that persists Bedrock state in a GitHub Gist.
1274
+ *
1275
+ * One gist holds one file per environment, named `state.<env>.json`. The
1276
+ * adapter authenticates with a user-supplied token and speaks the GitHub
1277
+ * REST API directly; no SDK dependency.
1278
+ *
1279
+ * @example
1280
+ *
1281
+ * ```ts
1282
+ * import { createGistStateAdapter } from "@bedrock-rbx/core";
1283
+ *
1284
+ * const port = createGistStateAdapter({
1285
+ * fetch: async () =>
1286
+ * new Response(JSON.stringify({ files: {} }), { status: 200 }),
1287
+ * gistId: "abc123def456",
1288
+ * token: "ghp_example",
1289
+ * });
1290
+ *
1291
+ * return port.read("production").then((result) => {
1292
+ * expect(result.success).toBeTrue();
1293
+ * if (result.success) {
1294
+ * expect(result.data).toBeUndefined();
1295
+ * }
1296
+ * });
1297
+ * ```
1298
+ *
1299
+ * @param deps - Gist ID, GitHub token, and optional fetch override.
1300
+ * @returns A `StatePort` ready to be passed to `deploy()`.
1301
+ */
1302
+ declare function createGistStateAdapter(deps: GistStateAdapterDeps): StatePort;
1303
+ //#endregion
1304
+ //#region src/adapters/place-driver.d.ts
1305
+ /**
1306
+ * Dependencies of `createPlaceDriver`. `universeId` is captured at
1307
+ * construction time (matching `GamePassDriverDeps`) so each driver instance
1308
+ * is bound to a single universe; multi-universe deploys construct one driver
1309
+ * per universe. `readFile` is injected because `diff` operates on file hashes
1310
+ * while the driver is the only place that needs the raw bytes.
1311
+ *
1312
+ * @example
1313
+ *
1314
+ * ```ts
1315
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
1316
+ * import { PlacesClient } from "@bedrock-rbx/ocale/places";
1317
+ * import { asRobloxAssetId, type PlaceDriverDeps } from "@bedrock-rbx/core";
1318
+ *
1319
+ * const httpClient: HttpClient = {
1320
+ * async request() {
1321
+ * return { data: { body: {}, headers: {}, status: 200 }, success: true };
1322
+ * },
1323
+ * };
1324
+ *
1325
+ * const deps: PlaceDriverDeps = {
1326
+ * client: new PlacesClient({
1327
+ * apiKey: "rbx-your-key",
1328
+ * httpClient,
1329
+ * sleep: async () => {},
1330
+ * }),
1331
+ * readFile: async () => new Uint8Array(),
1332
+ * universeId: asRobloxAssetId("1234567890"),
1333
+ * };
1334
+ *
1335
+ * expect(deps.universeId).toBe("1234567890");
1336
+ * ```
1337
+ */
1338
+ interface PlaceDriverDeps {
1339
+ /** Configured places client from `@bedrock-rbx/ocale/places`. */
1340
+ readonly client: PlacesClient;
1341
+ /** Reads place-file bytes for upload; rejections propagate out of the driver. */
1342
+ readonly readFile: (path: string) => Promise<Uint8Array>;
1343
+ /** Universe that owns every place this driver publishes. */
1344
+ readonly universeId: RobloxAssetId;
1345
+ }
1346
+ /**
1347
+ * Wraps {@link PlacesClient} as a `ResourceDriver<"place">`. `create` and
1348
+ * `update` are both thin wrappers over a shared publish helper because the
1349
+ * upstream Open Cloud call is identical either way: there is no "create
1350
+ * place" endpoint (the place is user-supplied input), only "publish version".
1351
+ *
1352
+ * Format is detected from the file extension (`.rbxl` → binary,
1353
+ * `.rbxlx` → XML); any other extension returns an `ApiError`-backed failure
1354
+ * without hitting the network.
1355
+ *
1356
+ * @param deps - Injected ocale client, file reader, and owning universe.
1357
+ * @returns A driver indexable by `"place"` in a `DriverRegistry`.
1358
+ * @throws Whatever `deps.readFile` rejects with.
1359
+ *
1360
+ * @example
1361
+ *
1362
+ * ```ts
1363
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
1364
+ * import { PlacesClient } from "@bedrock-rbx/ocale/places";
1365
+ * import {
1366
+ * asResourceKey,
1367
+ * asRobloxAssetId,
1368
+ * asSha256Hex,
1369
+ * createPlaceDriver,
1370
+ * } from "@bedrock-rbx/core";
1371
+ *
1372
+ * const httpClient: HttpClient = {
1373
+ * async request() {
1374
+ * return {
1375
+ * data: { body: { versionNumber: 1 }, headers: {}, status: 200 },
1376
+ * success: true,
1377
+ * };
1378
+ * },
1379
+ * };
1380
+ *
1381
+ * const driver = createPlaceDriver({
1382
+ * client: new PlacesClient({
1383
+ * apiKey: "rbx-your-key",
1384
+ * httpClient,
1385
+ * sleep: async () => {},
1386
+ * }),
1387
+ * readFile: async () =>
1388
+ * new Uint8Array([
1389
+ * 0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x21, 0x89, 0xff, 0x0d, 0x0a, 0x1a,
1390
+ * 0x0a,
1391
+ * ]),
1392
+ * universeId: asRobloxAssetId("1234567890"),
1393
+ * });
1394
+ *
1395
+ * return driver
1396
+ * .create({
1397
+ * description: undefined,
1398
+ * displayName: undefined,
1399
+ * fileHash: asSha256Hex(
1400
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1401
+ * ),
1402
+ * filePath: "places/start.rbxl",
1403
+ * key: asResourceKey("start-place"),
1404
+ * kind: "place",
1405
+ * placeId: asRobloxAssetId("4711"),
1406
+ * serverSize: undefined,
1407
+ * })
1408
+ * .then((result) => {
1409
+ * expect(result.success).toBeTrue();
1410
+ * if (result.success) {
1411
+ * expect(result.data.outputs.versionNumber).toBe(1);
1412
+ * }
1413
+ * });
1414
+ * ```
1415
+ */
1416
+ declare function createPlaceDriver(deps: PlaceDriverDeps): ResourceDriver<"place">;
1417
+ //#endregion
1418
+ //#region src/adapters/universe-driver.d.ts
1419
+ /**
1420
+ * Dependencies of `createUniverseDriver`. The driver reconciles the
1421
+ * universe singleton against both the universes endpoint and the root
1422
+ * place (for fields Roblox marks read-only on the universe, like
1423
+ * `displayName`). There is no `universeId` at construction time because
1424
+ * the universe *is* the resource the driver reconciles, so the ID rides
1425
+ * along on each `UniverseDesiredState`.
1426
+ */
1427
+ interface UniverseDriverDeps {
1428
+ /** Configured places client from `@bedrock-rbx/ocale/places`. */
1429
+ readonly places: PlacesClient;
1430
+ /** Reads icon bytes for upload; rejections propagate out of `create`/`update`. */
1431
+ readonly readFile: (path: string) => Promise<Uint8Array>;
1432
+ /**
1433
+ * Configured universes client from `@bedrock-rbx/ocale/universes`. Localized
1434
+ * experience-icon Operations are reached through `universes.icon.*`.
1435
+ */
1436
+ readonly universes: UniversesClient;
1437
+ }
1438
+ /**
1439
+ * Wraps {@link UniversesClient} as a `ResourceDriver<"universe">`. `create`
1440
+ * and `update` both delegate to a shared reconcile helper because Open
1441
+ * Cloud cannot mint universes; the user supplies an existing `universeId`
1442
+ * and bedrock adopts the universe on first apply.
1443
+ *
1444
+ * A `NotFound` error (HTTP 404) from `UniversesClient.update` is repackaged
1445
+ * as an adoption-error `ApiError` whose message names the config key and
1446
+ * the `universeId`, so operators can tell adoption failure apart from
1447
+ * transient upstream errors. A successful response whose `rootPlaceId` is
1448
+ * absent surfaces as an `ApiError` with status 200, mirroring the
1449
+ * malformed-response guard in `GamePassDriver`.
1450
+ *
1451
+ * When `displayName` is declared, the driver routes that field through
1452
+ * `PlacesClient.update` on the root place after the universe PATCH
1453
+ * succeeds. A subsequent places failure surfaces to the caller as the
1454
+ * driver's error result without rolling back the prior universe patch,
1455
+ * so callers observing a partial failure should reconcile by
1456
+ * reapplying rather than assuming the universe-level fields are
1457
+ * unchanged.
1458
+ *
1459
+ * @param deps - Injected ocale clients (universes plus places for the
1460
+ * read-only universe fields Roblox derives from the root place).
1461
+ * @returns A driver indexable by `"universe"` in a `DriverRegistry`.
1462
+ *
1463
+ * @example
1464
+ *
1465
+ * ```ts
1466
+ * import type { HttpClient } from "@bedrock-rbx/ocale";
1467
+ * import { PlacesClient } from "@bedrock-rbx/ocale/places";
1468
+ * import { UniversesClient } from "@bedrock-rbx/ocale/universes";
1469
+ * import { validUniverseBody } from "@bedrock-rbx/ocale/testing";
1470
+ * import {
1471
+ * asRobloxAssetId,
1472
+ * createUniverseDriver,
1473
+ * UNIVERSE_SINGLETON_KEY,
1474
+ * } from "@bedrock-rbx/core";
1475
+ *
1476
+ * const universeBodyHttpClient: HttpClient = {
1477
+ * async request() {
1478
+ * return {
1479
+ * data: {
1480
+ * body: validUniverseBody({
1481
+ * path: "universes/1234567890",
1482
+ * rootPlace: "universes/1234567890/places/4711",
1483
+ * }),
1484
+ * headers: {},
1485
+ * status: 200,
1486
+ * },
1487
+ * success: true,
1488
+ * };
1489
+ * },
1490
+ * };
1491
+ *
1492
+ * const driver = createUniverseDriver({
1493
+ * places: new PlacesClient({
1494
+ * apiKey: "rbx-your-key",
1495
+ * httpClient: universeBodyHttpClient,
1496
+ * sleep: async () => {},
1497
+ * }),
1498
+ * readFile: async () => new Uint8Array(),
1499
+ * universes: new UniversesClient({
1500
+ * apiKey: "rbx-your-key",
1501
+ * httpClient: universeBodyHttpClient,
1502
+ * sleep: async () => {},
1503
+ * }),
1504
+ * });
1505
+ *
1506
+ * return driver
1507
+ * .create({
1508
+ * consoleEnabled: undefined,
1509
+ * desktopEnabled: true,
1510
+ * displayName: undefined,
1511
+ * key: UNIVERSE_SINGLETON_KEY,
1512
+ * kind: "universe",
1513
+ * mobileEnabled: undefined,
1514
+ * privateServerPriceRobux: undefined,
1515
+ * tabletEnabled: undefined,
1516
+ * universeId: asRobloxAssetId("1234567890"),
1517
+ * voiceChatEnabled: true,
1518
+ * vrEnabled: undefined,
1519
+ * })
1520
+ * .then((result) => {
1521
+ * expect(result.success).toBeTrue();
1522
+ * if (result.success) {
1523
+ * expect(result.data.outputs.rootPlaceId).toBe("4711");
1524
+ * }
1525
+ * });
1526
+ * ```
1527
+ */
1528
+ declare function createUniverseDriver(deps: UniverseDriverDeps): ResourceDriver<"universe">;
1529
+ //#endregion
1530
+ //#region src/core/derive-price-fields.d.ts
1531
+ /**
1532
+ * Wire-shape pricing fragment produced by {@link derivePriceFields}: the
1533
+ * `isForSale` flag and an optional numeric `price`. Mirrors the multipart
1534
+ * fields the Open Cloud `developer-products` create and update endpoints
1535
+ * accept for setting Robux pricing.
1536
+ */
1537
+ interface PriceFields {
1538
+ /** Whether the developer product should be purchasable. */
1539
+ readonly isForSale: boolean;
1540
+ /** Default price in Robux; absent when the product is off-sale. */
1541
+ readonly price?: number;
1542
+ }
1543
+ /**
1544
+ * Translate a Mantle-style optional price into the Open Cloud wire shape.
1545
+ *
1546
+ * `desired.price === undefined` (no price declared) becomes
1547
+ * `{ isForSale: false }` and the `price` key is omitted entirely. A defined
1548
+ * price (including `0`) becomes `{ isForSale: true, price }`. Both
1549
+ * `developerProduct` create and update paths share this helper so the
1550
+ * "absent ⇒ off-sale" semantics live in exactly one place.
1551
+ *
1552
+ * @param desired - Object carrying the user-declared `price`.
1553
+ * @returns The wire-shape `{ isForSale, price? }` fragment.
1554
+ *
1555
+ * @example
1556
+ *
1557
+ * ```ts
1558
+ * import { derivePriceFields } from "@bedrock-rbx/core";
1559
+ *
1560
+ * expect(derivePriceFields({ price: undefined })).toStrictEqual({ isForSale: false });
1561
+ * expect(derivePriceFields({ price: 250 })).toStrictEqual({ isForSale: true, price: 250 });
1562
+ * ```
1563
+ */
1564
+ declare function derivePriceFields(desired: {
1565
+ readonly price: number | undefined;
1566
+ }): PriceFields;
1567
+ //#endregion
1568
+ //#region src/core/operations.d.ts
1569
+ /**
1570
+ * Fields shared by every operation variant.
1571
+ *
1572
+ * `key` is hoisted to op-level (rather than nested under `desired` or
1573
+ * `current`) so callers can read it from any variant without first narrowing
1574
+ * on the discriminator. `applyOps` and logging both rely on this uniform
1575
+ * access pattern.
1576
+ *
1577
+ * @example
1578
+ *
1579
+ * ```ts
1580
+ * import { asResourceKey, type BaseOperation } from "@bedrock-rbx/core";
1581
+ *
1582
+ * const base: BaseOperation = { key: asResourceKey("vip-pass") };
1583
+ *
1584
+ * expect(base.key).toBe("vip-pass");
1585
+ * ```
1586
+ */
1587
+ interface BaseOperation {
1588
+ /** Resource key copied from the desired or current entry the op describes. */
1589
+ readonly key: ResourceKey;
1590
+ }
1591
+ /**
1592
+ * Reconcile an absent resource: produced when a `desired` entry has no
1593
+ * matching `current` entry. The driver creates the resource and records the
1594
+ * Roblox-assigned outputs into state.
1595
+ *
1596
+ * @example
1597
+ *
1598
+ * ```ts
1599
+ * import {
1600
+ * asResourceKey,
1601
+ * asSha256Hex,
1602
+ * type CreateOperation,
1603
+ * } from "@bedrock-rbx/core";
1604
+ *
1605
+ * const op: CreateOperation = {
1606
+ * desired: {
1607
+ * description: "Grants VIP perks.",
1608
+ * icon: { "en-us": "assets/vip-icon.png" },
1609
+ * iconFileHashes: {
1610
+ * "en-us": asSha256Hex(
1611
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1612
+ * ),
1613
+ * },
1614
+ * key: asResourceKey("vip-pass"),
1615
+ * kind: "gamePass",
1616
+ * name: "VIP Pass",
1617
+ * price: 500,
1618
+ * },
1619
+ * key: asResourceKey("vip-pass"),
1620
+ * type: "create",
1621
+ * };
1622
+ *
1623
+ * expect(op.type).toBe("create");
1624
+ * expect(op.desired.kind).toBe("gamePass");
1625
+ * ```
1626
+ */
1627
+ interface CreateOperation extends BaseOperation {
1628
+ /** Declared desired state to materialize through the driver. */
1629
+ readonly desired: ResourceDesiredState;
1630
+ /** Discriminator tag for the `Operation` union. */
1631
+ readonly type: "create";
1632
+ }
1633
+ /**
1634
+ * Reconcile a drifted resource: produced when a `desired` entry differs from
1635
+ * its matching `current` entry. Both states are carried so the driver can
1636
+ * compute the minimal patch.
1637
+ *
1638
+ * @example
1639
+ *
1640
+ * ```ts
1641
+ * import {
1642
+ * asResourceKey,
1643
+ * asRobloxAssetId,
1644
+ * asSha256Hex,
1645
+ * type UpdateOperation,
1646
+ * } from "@bedrock-rbx/core";
1647
+ *
1648
+ * const op: UpdateOperation = {
1649
+ * current: {
1650
+ * description: "Grants VIP perks.",
1651
+ * icon: { "en-us": "assets/vip-icon.png" },
1652
+ * iconFileHashes: {
1653
+ * "en-us": asSha256Hex(
1654
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1655
+ * ),
1656
+ * },
1657
+ * key: asResourceKey("vip-pass"),
1658
+ * kind: "gamePass",
1659
+ * name: "VIP Pass",
1660
+ * outputs: {
1661
+ * assetId: asRobloxAssetId("9876543210"),
1662
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
1663
+ * },
1664
+ * price: 500,
1665
+ * },
1666
+ * desired: {
1667
+ * description: "Grants VIP perks plus emote.",
1668
+ * icon: { "en-us": "assets/vip-icon.png" },
1669
+ * iconFileHashes: {
1670
+ * "en-us": asSha256Hex(
1671
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1672
+ * ),
1673
+ * },
1674
+ * key: asResourceKey("vip-pass"),
1675
+ * kind: "gamePass",
1676
+ * name: "VIP Pass",
1677
+ * price: 750,
1678
+ * },
1679
+ * key: asResourceKey("vip-pass"),
1680
+ * type: "update",
1681
+ * };
1682
+ *
1683
+ * expect(op.type).toBe("update");
1684
+ * if (op.desired.kind === "gamePass") {
1685
+ * expect(op.desired.price).toBe(750);
1686
+ * }
1687
+ * if (op.current.kind === "gamePass") {
1688
+ * expect(op.current.outputs.assetId).toBe("9876543210");
1689
+ * }
1690
+ * ```
1691
+ */
1692
+ interface UpdateOperation extends BaseOperation {
1693
+ /** Last-known live state; the driver computes a patch against `desired`. */
1694
+ readonly current: ResourceCurrentState;
1695
+ /** Declared desired state to converge toward. */
1696
+ readonly desired: ResourceDesiredState;
1697
+ /** Discriminator tag for the `Operation` union. */
1698
+ readonly type: "update";
1699
+ }
1700
+ /**
1701
+ * Acknowledge that a resource is already in sync: produced when a `desired`
1702
+ * entry matches its `current` entry exactly. The driver performs no I/O for
1703
+ * this variant.
1704
+ *
1705
+ * Bare by design: the operation carries only `key` and `type` because no
1706
+ * payload is needed at apply time. Callers that need the matching desired or
1707
+ * current state look it up in the snapshots passed to `diff`.
1708
+ *
1709
+ * @example
1710
+ *
1711
+ * ```ts
1712
+ * import { asResourceKey, type NoopOperation } from "@bedrock-rbx/core";
1713
+ *
1714
+ * const op: NoopOperation = {
1715
+ * key: asResourceKey("vip-pass"),
1716
+ * type: "noop",
1717
+ * };
1718
+ *
1719
+ * expect(op.type).toBe("noop");
1720
+ * expect(op.key).toBe("vip-pass");
1721
+ * ```
1722
+ */
1723
+ interface NoopOperation extends BaseOperation {
1724
+ /** Discriminator tag for the `Operation` union. */
1725
+ readonly type: "noop";
1726
+ }
1727
+ /**
1728
+ * Discriminated union of every reconciliation step `diff` produces and
1729
+ * `applyOps` consumes. The `type` field is the discriminator (`kind` is
1730
+ * reserved for the resource discriminator in `ResourceDesiredState`).
1731
+ *
1732
+ * A `delete` variant is intentionally absent: resources present only in
1733
+ * current state (orphans) are ignored, never reconciled.
1734
+ *
1735
+ * @example
1736
+ *
1737
+ * ```ts
1738
+ * import { asResourceKey, type Operation } from "@bedrock-rbx/core";
1739
+ *
1740
+ * function describeOp(op: Operation): string {
1741
+ * switch (op.type) {
1742
+ * case "create": {
1743
+ * return `create ${op.desired.kind} ${op.key}`;
1744
+ * }
1745
+ * case "update": {
1746
+ * return `update ${op.desired.kind} ${op.key}`;
1747
+ * }
1748
+ * case "noop": {
1749
+ * return `noop ${op.key}`;
1750
+ * }
1751
+ * }
1752
+ * }
1753
+ *
1754
+ * const op: Operation = {
1755
+ * key: asResourceKey("vip-pass"),
1756
+ * type: "noop",
1757
+ * };
1758
+ *
1759
+ * expect(describeOp(op)).toBe("noop vip-pass");
1760
+ * ```
1761
+ */
1762
+ type Operation = CreateOperation | NoopOperation | UpdateOperation;
1763
+ //#endregion
1764
+ //#region src/core/diff.d.ts
1765
+ /**
1766
+ * Computes the operations required to reconcile `current` state with `desired`
1767
+ * state. Pure and synchronous: no I/O, no side effects, no `Result` wrapper.
1768
+ *
1769
+ * Each entry in `desired` is matched to `current` by `(kind, key)`: resources
1770
+ * are uniquely identified by that pair, so a `place` and a `universe` keyed
1771
+ * `"main"` are independent slots. A `(kind, key)` pair present only in
1772
+ * `desired` produces a `create` op; a pair present in both produces an
1773
+ * `update` op if any declared field differs or a `noop` op if every field
1774
+ * matches.
1775
+ *
1776
+ * Ops appear in the order their desired entries appear in the input array so
1777
+ * callers can rely on declaration order when logging or applying ops.
1778
+ *
1779
+ * @param desired - Declared desired state from user config, already normalized
1780
+ * (file hashes computed, nullable wire values mapped to `undefined`).
1781
+ * @param current - Last-known live state from the state file.
1782
+ * @returns Operations to reconcile the two snapshots.
1783
+ *
1784
+ * @example
1785
+ *
1786
+ * ```ts
1787
+ * import {
1788
+ * asResourceKey,
1789
+ * asRobloxAssetId,
1790
+ * asSha256Hex,
1791
+ * diff,
1792
+ * type GamePassDesiredState,
1793
+ * type ResourceCurrentState,
1794
+ * } from "@bedrock-rbx/core";
1795
+ *
1796
+ * const hash = asSha256Hex(
1797
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1798
+ * );
1799
+ *
1800
+ * const unchanged: GamePassDesiredState = {
1801
+ * description: "Grants VIP perks.",
1802
+ * icon: { "en-us": "assets/vip-icon.png" },
1803
+ * iconFileHashes: { "en-us": hash },
1804
+ * key: asResourceKey("vip-pass"),
1805
+ * kind: "gamePass",
1806
+ * name: "VIP Pass",
1807
+ * price: 500,
1808
+ * };
1809
+ * const drifted: GamePassDesiredState = {
1810
+ * ...unchanged,
1811
+ * key: asResourceKey("legend-pass"),
1812
+ * name: "Legend Pass (renamed)",
1813
+ * };
1814
+ * const fresh: GamePassDesiredState = {
1815
+ * ...unchanged,
1816
+ * key: asResourceKey("rookie-pass"),
1817
+ * name: "Rookie Pass",
1818
+ * };
1819
+ *
1820
+ * const current: ReadonlyArray<ResourceCurrentState> = [
1821
+ * {
1822
+ * ...unchanged,
1823
+ * outputs: {
1824
+ * assetId: asRobloxAssetId("111"),
1825
+ * iconAssetIds: { "en-us": asRobloxAssetId("222") },
1826
+ * },
1827
+ * },
1828
+ * {
1829
+ * ...drifted,
1830
+ * name: "Legend Pass",
1831
+ * outputs: {
1832
+ * assetId: asRobloxAssetId("333"),
1833
+ * iconAssetIds: { "en-us": asRobloxAssetId("444") },
1834
+ * },
1835
+ * },
1836
+ * ];
1837
+ *
1838
+ * const ops = diff([unchanged, drifted, fresh], current);
1839
+ *
1840
+ * expect(ops.map((op) => op.type)).toEqual(["noop", "update", "create"]);
1841
+ * ```
1842
+ */
1843
+ declare function diff(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): ReadonlyArray<Operation>;
1844
+ //#endregion
1845
+ //#region src/core/display-name-prefix.d.ts
1846
+ /**
1847
+ * Default template applied when a project enables display-name prefixing
1848
+ * without supplying its own `displayNamePrefix.format`. Yields outputs
1849
+ * like `[STAGING] ` for an environment whose `label` is `"staging"`.
1850
+ */
1851
+ declare const DEFAULT_PREFIX_FORMAT = "[{LABEL}] ";
1852
+ /**
1853
+ * Render the prefix that selectEnvironment prepends to declared display
1854
+ * names when a project enables `displayNamePrefix`. The template
1855
+ * recognizes three placeholders:
1856
+ *
1857
+ * - `{label}`: label as written.
1858
+ * - `{LABEL}`: upper-cased label.
1859
+ * - `{Label}`: capitalized label (first character upper, rest as written).
1860
+ *
1861
+ * Other characters in the template flow through verbatim.
1862
+ *
1863
+ * @param label - Environment label declared on `EnvironmentEntry.label`.
1864
+ * @param format - Template string. Falls back to
1865
+ * {@link DEFAULT_PREFIX_FORMAT} when omitted.
1866
+ * @returns The rendered prefix string.
1867
+ *
1868
+ * @example
1869
+ *
1870
+ * ```ts
1871
+ * import { renderDisplayNamePrefix } from "@bedrock-rbx/core";
1872
+ *
1873
+ * expect(renderDisplayNamePrefix("staging")).toBe("[STAGING] ");
1874
+ * expect(renderDisplayNamePrefix("staging", "{Label}: ")).toBe("Staging: ");
1875
+ * expect(renderDisplayNamePrefix("dev", "{LABEL}-{label}")).toBe("DEV-dev");
1876
+ * ```
1877
+ */
1878
+ declare function renderDisplayNamePrefix(label: string, format?: string): string;
1879
+ //#endregion
1880
+ //#region src/core/environment.d.ts
1881
+ /**
1882
+ * Validate an environment name at a state-adapter boundary.
1883
+ *
1884
+ * Adapters that map environment names onto filesystem-like identifiers
1885
+ * (gist filenames, S3 keys) must reject names that could collide or escape
1886
+ * their storage layout. This helper accepts letters, digits, `-`, and `_`
1887
+ * only, with length between 1 and 64, and returns a `StateError` for
1888
+ * anything outside that set so the adapter can fail loudly instead of
1889
+ * silently stripping characters.
1890
+ *
1891
+ * @example
1892
+ *
1893
+ * ```ts
1894
+ * import { validateEnvironmentName } from "@bedrock-rbx/core";
1895
+ *
1896
+ * const ok = validateEnvironmentName("production");
1897
+ * expect(ok.success).toBeTrue();
1898
+ *
1899
+ * const bad = validateEnvironmentName("prod/staging");
1900
+ * expect(bad.success).toBeFalse();
1901
+ * ```
1902
+ *
1903
+ * @param environment - Raw environment name supplied by a caller.
1904
+ * @returns `Ok(environment)` when the name is safe to use, or
1905
+ * `Err(StateError)` with a descriptive reason when it is not.
1906
+ */
1907
+ declare function validateEnvironmentName(environment: string): Result$1<string, StateError>;
1908
+ //#endregion
1909
+ //#region src/core/flatten.d.ts
1910
+ /**
1911
+ * Pre-I/O game-pass input the flattener emits. Extends the authored
1912
+ * `GamePassEntry` with the tag discriminator and the `ResourceKey`-branded
1913
+ * key so `buildDesired` can consume a flat tagged list and layer on the
1914
+ * SHA-256 icon digest.
1915
+ *
1916
+ * @example
1917
+ *
1918
+ * ```ts
1919
+ * import { asResourceKey, type GamePassDesiredInput } from "@bedrock-rbx/core";
1920
+ *
1921
+ * const input: GamePassDesiredInput = {
1922
+ * description: "Grants VIP perks.",
1923
+ * icon: { "en-us": "assets/vip-icon.png" },
1924
+ * key: asResourceKey("vip-pass"),
1925
+ * kind: "gamePass",
1926
+ * name: "VIP Pass",
1927
+ * price: 500,
1928
+ * };
1929
+ *
1930
+ * expect(input.kind).toBe("gamePass");
1931
+ * ```
1932
+ */
1933
+ interface GamePassDesiredInput extends Readonly<GamePassEntry> {
1934
+ /** User-supplied handle, already validated against the `ResourceKey` brand. */
1935
+ readonly key: ResourceKey;
1936
+ /** Discriminator tag for the `ResourceDesiredInput` union. */
1937
+ readonly kind: "gamePass";
1938
+ }
1939
+ /**
1940
+ * Pre-I/O place input the flattener emits. Carries the resolved place
1941
+ * fields (`filePath` from the root, `placeId` from the per-environment
1942
+ * overlay) plus the optional metadata fields and the tag discriminator and
1943
+ * the `ResourceKey`-branded key, so `buildDesired` can consume a flat
1944
+ * tagged list and layer on the SHA-256 file digest.
1945
+ *
1946
+ * @example
1947
+ *
1948
+ * ```ts
1949
+ * import { asResourceKey, asRobloxAssetId, type PlaceDesiredInput } from "@bedrock-rbx/core";
1950
+ *
1951
+ * const input: PlaceDesiredInput = {
1952
+ * description: undefined,
1953
+ * displayName: "Start Place",
1954
+ * filePath: "places/start.rbxl",
1955
+ * key: asResourceKey("start-place"),
1956
+ * kind: "place",
1957
+ * placeId: asRobloxAssetId("4711"),
1958
+ * serverSize: 50,
1959
+ * };
1960
+ *
1961
+ * expect(input.kind).toBe("place");
1962
+ * expect(input.displayName).toBe("Start Place");
1963
+ * ```
1964
+ */
1965
+ interface PlaceDesiredInput {
1966
+ /** User-supplied handle, already validated against the `ResourceKey` brand. */
1967
+ readonly key: ResourceKey;
1968
+ /** User-facing place description; `undefined` leaves the server value untouched. */
1969
+ readonly description: string | undefined;
1970
+ /** User-facing place name; `undefined` leaves the server value untouched. */
1971
+ readonly displayName: string | undefined;
1972
+ /** Path to the `.rbxl` or `.rbxlx` file; read by `buildDesired`. */
1973
+ readonly filePath: string;
1974
+ /** Discriminator tag for the `ResourceDesiredInput` union. */
1975
+ readonly kind: "place";
1976
+ /** Existing Roblox place ID, validated and branded at flatten time. */
1977
+ readonly placeId: RobloxAssetId;
1978
+ /** Maximum players per server; `undefined` leaves the server value untouched. */
1979
+ readonly serverSize: number | undefined;
1980
+ }
1981
+ /**
1982
+ * Pre-I/O universe input the flattener emits. Carries the fixed singleton
1983
+ * key (`"main"`) and the branded `universeId` so `buildDesired` can hand a
1984
+ * single tagged record downstream without a shape divergence for the
1985
+ * singleton kind.
1986
+ *
1987
+ * @example
1988
+ *
1989
+ * ```ts
1990
+ * import { asRobloxAssetId, UNIVERSE_SINGLETON_KEY, type UniverseDesiredInput } from "@bedrock-rbx/core";
1991
+ *
1992
+ * const input: UniverseDesiredInput = {
1993
+ * consoleEnabled: undefined,
1994
+ * desktopEnabled: true,
1995
+ * displayName: undefined,
1996
+ * key: UNIVERSE_SINGLETON_KEY,
1997
+ * kind: "universe",
1998
+ * mobileEnabled: undefined,
1999
+ * tabletEnabled: undefined,
2000
+ * universeId: asRobloxAssetId("1234567890"),
2001
+ * voiceChatEnabled: true,
2002
+ * vrEnabled: undefined,
2003
+ * };
2004
+ *
2005
+ * expect(input.kind).toBe("universe");
2006
+ * expect(input.key).toBe("main");
2007
+ * expect(input.desktopEnabled).toBeTrue();
2008
+ * ```
2009
+ */
2010
+ interface UniverseDesiredInput {
2011
+ /** Synthesized singleton key (`"main"`), already validated against the `ResourceKey` brand. */
2012
+ readonly key: ResourceKey;
2013
+ /** Whether console players can join; `undefined` leaves the server value untouched. */
2014
+ readonly consoleEnabled: boolean | undefined;
2015
+ /** Whether desktop players can join; `undefined` leaves the server value untouched. */
2016
+ readonly desktopEnabled: boolean | undefined;
2017
+ /** Discord social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2018
+ readonly discordSocialLink?: SocialLink$1 | undefined;
2019
+ /**
2020
+ * Display name for the universe. `undefined` leaves the server value
2021
+ * untouched. The driver routes declared updates through
2022
+ * `PlacesClient.update` because the universe PATCH endpoint treats
2023
+ * `displayName` as read-only.
2024
+ */
2025
+ readonly displayName: string | undefined;
2026
+ /** Facebook social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2027
+ readonly facebookSocialLink?: SocialLink$1 | undefined;
2028
+ /** Guilded social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2029
+ readonly guildedSocialLink?: SocialLink$1 | undefined;
2030
+ /**
2031
+ * Locale-keyed experience-icon paths copied from the user-supplied
2032
+ * `UniverseEntry`. Absent when the user did not declare an icon block.
2033
+ */
2034
+ readonly icon?: Record<"en-us", string>;
2035
+ /** Discriminator tag for the `ResourceDesiredInput` union. */
2036
+ readonly kind: "universe";
2037
+ /** Whether mobile players can join; `undefined` leaves the server value untouched. */
2038
+ readonly mobileEnabled: boolean | undefined;
2039
+ /**
2040
+ * Private-server price in Robux. A present key with `undefined`
2041
+ * clears the server value (ocale emits JSON `null`); an absent key
2042
+ * leaves the server value untouched.
2043
+ */
2044
+ readonly privateServerPriceRobux?: number | undefined;
2045
+ /** Roblox Group social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2046
+ readonly robloxGroupSocialLink?: SocialLink$1 | undefined;
2047
+ /** Whether tablet players can join; `undefined` leaves the server value untouched. */
2048
+ readonly tabletEnabled: boolean | undefined;
2049
+ /** Twitch social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2050
+ readonly twitchSocialLink?: SocialLink$1 | undefined;
2051
+ /** Twitter social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2052
+ readonly twitterSocialLink?: SocialLink$1 | undefined;
2053
+ /** Existing Roblox universe ID, validated and branded at flatten time. */
2054
+ readonly universeId: RobloxAssetId;
2055
+ /** Whether voice chat is enabled; `undefined` leaves the server value untouched. */
2056
+ readonly voiceChatEnabled: boolean | undefined;
2057
+ /** Whether VR players can join; `undefined` leaves the server value untouched. */
2058
+ readonly vrEnabled: boolean | undefined;
2059
+ /** YouTube social link; tri-state (absent/undefined/set) — see `UniverseDesiredState`. */
2060
+ readonly youtubeSocialLink?: SocialLink$1 | undefined;
2061
+ }
2062
+ /**
2063
+ * Pre-I/O developer-product input the flattener emits. Extends the authored
2064
+ * `DeveloperProductEntry` with the tag discriminator and the
2065
+ * `ResourceKey`-branded key so `buildDesired` can consume a flat tagged
2066
+ * list.
2067
+ *
2068
+ * @example
2069
+ *
2070
+ * ```ts
2071
+ * import { asResourceKey, type DeveloperProductDesiredInput } from "@bedrock-rbx/core";
2072
+ *
2073
+ * const input: DeveloperProductDesiredInput = {
2074
+ * description: "Stocks the player up with 1,000 premium gems.",
2075
+ * key: asResourceKey("gem-pack"),
2076
+ * kind: "developerProduct",
2077
+ * name: "Gem Pack",
2078
+ * };
2079
+ *
2080
+ * expect(input.kind).toBe("developerProduct");
2081
+ * ```
2082
+ */
2083
+ interface DeveloperProductDesiredInput extends Readonly<DeveloperProductEntry> {
2084
+ /** User-supplied handle, already validated against the `ResourceKey` brand. */
2085
+ readonly key: ResourceKey;
2086
+ /** Discriminator tag for the `ResourceDesiredInput` union. */
2087
+ readonly kind: "developerProduct";
2088
+ }
2089
+ /**
2090
+ * Flat tagged input for `buildDesired`. One member per resource kind; future
2091
+ * kinds widen this union as they land.
2092
+ */
2093
+ type ResourceDesiredInput = DeveloperProductDesiredInput | GamePassDesiredInput | PlaceDesiredInput | UniverseDesiredInput;
2094
+ /**
2095
+ * Turn a resolved `Config` into a flat, tagged list of resource inputs.
2096
+ *
2097
+ * Pure and infallible: validation and per-environment overlay merging
2098
+ * have already happened upstream (typically via `selectEnvironment`), so
2099
+ * every invariant this function relies on is guaranteed by the input
2100
+ * shape. Entries appear in the insertion order of each collection;
2101
+ * `passes` are emitted before `places`.
2102
+ *
2103
+ * @param config - Resolved config returned by `selectEnvironment`.
2104
+ * @returns Flat tagged list ready for `buildDesired`.
2105
+ * @example
2106
+ *
2107
+ * ```ts
2108
+ * import { flattenConfig, selectEnvironment, type Config } from "@bedrock-rbx/core";
2109
+ *
2110
+ * const config: Config = {
2111
+ * environments: {
2112
+ * production: { places: { "start-place": { placeId: "4711" } } },
2113
+ * },
2114
+ * passes: {
2115
+ * "vip-pass": {
2116
+ * description: "Grants VIP perks.",
2117
+ * icon: { "en-us": "assets/vip-icon.png" },
2118
+ * name: "VIP Pass",
2119
+ * price: 500,
2120
+ * },
2121
+ * },
2122
+ * places: { "start-place": { filePath: "places/start.rbxl" } },
2123
+ * };
2124
+ *
2125
+ * const resolved = selectEnvironment(config, "production");
2126
+ * expect(resolved.success).toBeTrue();
2127
+ * if (resolved.success) {
2128
+ * const inputs = flattenConfig(resolved.data);
2129
+ * expect(inputs.map((input) => input.kind)).toEqual(["gamePass", "place"]);
2130
+ * expect(inputs.map((input) => input.key)).toEqual(["vip-pass", "start-place"]);
2131
+ * }
2132
+ * ```
2133
+ */
2134
+ declare function flattenConfig(config: ResolvedConfig): ReadonlyArray<ResourceDesiredInput>;
2135
+ //#endregion
2136
+ //#region src/core/get-environment.d.ts
2137
+ /**
2138
+ * Failure modes returned by {@link getEnvironment}.
2139
+ */
2140
+ type GetEnvironmentError = {
2141
+ readonly kind: "missingEnvironment";
2142
+ } | {
2143
+ readonly kind: "multipleEnvironments";
2144
+ readonly values: ReadonlyArray<string>;
2145
+ };
2146
+ /**
2147
+ * Resolve the deploy environment for an override script invocation.
2148
+ *
2149
+ * Reads `--env <name>` from the supplied argv first, falls back to
2150
+ * `BEDROCK_ENVIRONMENT` from the supplied env reader. Returns
2151
+ * `missingEnvironment` when neither is present and `multipleEnvironments`
2152
+ * (with every offending value) when argv contains more than one `--env`
2153
+ * flag. Both inputs default to the running process so override scripts
2154
+ * under `.bedrock/` can call `getEnvironment()` with no arguments.
2155
+ *
2156
+ * @param argv - Argument list to scan for `--env <name>` flags. Defaults to
2157
+ * `process.argv.slice(2)` when omitted.
2158
+ * @param readEnvironment - Reads an environment variable; consulted as a
2159
+ * fallback when no `--env` flag is present. Defaults to a `process.env`
2160
+ * reader when omitted.
2161
+ * @returns `Ok(environment)` on success, `Err(GetEnvironmentError)` otherwise.
2162
+ * @example
2163
+ *
2164
+ * ```ts
2165
+ * import { getEnvironment } from "@bedrock-rbx/core";
2166
+ *
2167
+ * const result = getEnvironment(["--env", "production"], () => undefined);
2168
+ *
2169
+ * expect(result.success).toBeTrue();
2170
+ * if (result.success) {
2171
+ * expect(result.data).toBe("production");
2172
+ * }
2173
+ * ```
2174
+ */
2175
+ declare function getEnvironment(argv?: ReadonlyArray<string>, readEnvironment?: (name: string) => string | undefined): Result$1<string, GetEnvironmentError>;
2176
+ //#endregion
2177
+ //#region src/core/kinds/module.d.ts
2178
+ /**
2179
+ * Failure surfaced during desired-state preparation. Two variants today:
2180
+ *
2181
+ * - `fileReadFailed`: a kind module's `normalize` could not read a file
2182
+ * the input declared (e.g. An icon path that is missing on disk).
2183
+ * - `iconRemovalRejected`: `validatePlan` saw a kind whose prior current
2184
+ * state recorded an icon that the desired state no longer declares,
2185
+ * and the kind has no documented unset path on the upstream API.
2186
+ *
2187
+ * Both variants carry the offending `key` so the CLI can attribute the
2188
+ * failure to a single resource entry.
2189
+ *
2190
+ * @example
2191
+ *
2192
+ * ```ts
2193
+ * import { asResourceKey, type BuildDesiredError } from "@bedrock-rbx/core";
2194
+ *
2195
+ * const err: BuildDesiredError = {
2196
+ * filePath: "assets/vip-icon.png",
2197
+ * key: asResourceKey("vip-pass"),
2198
+ * kind: "fileReadFailed",
2199
+ * reason: "ENOENT",
2200
+ * };
2201
+ *
2202
+ * expect(err.kind).toBe("fileReadFailed");
2203
+ * ```
2204
+ */
2205
+ type BuildDesiredError = {
2206
+ /** Path of the file that failed to read. */readonly filePath: string; /** ResourceKey of the input whose file failed to read. */
2207
+ readonly key: ResourceKey; /** Literal discriminator for narrowing. */
2208
+ readonly kind: "fileReadFailed"; /** Human-readable explanation; typically the caught error message. */
2209
+ readonly reason: string;
2210
+ } | {
2211
+ /** ResourceKey of the entry whose icon is being removed. */readonly key: ResourceKey; /** Literal discriminator for narrowing. */
2212
+ readonly kind: "iconRemovalRejected"; /** Human-readable explanation naming the resource and the invariant. */
2213
+ readonly message: string;
2214
+ };
2215
+ /**
2216
+ * I/O surface the shell injects into kind-module `normalize` calls. Carries
2217
+ * only file-reading capability today; new capabilities widen this shape
2218
+ * when a kind module needs them.
2219
+ *
2220
+ * @example
2221
+ *
2222
+ * ```ts
2223
+ * import type { KindIo } from "@bedrock-rbx/core";
2224
+ *
2225
+ * const io: KindIo = {
2226
+ * readFile: async () => new Uint8Array([1, 2, 3]),
2227
+ * };
2228
+ *
2229
+ * expect(io.readFile).toBeFunction();
2230
+ * ```
2231
+ */
2232
+ interface KindIo {
2233
+ /** Reads file bytes for a given path; rejection becomes a `fileReadFailed` Err. */
2234
+ readonly readFile: (path: string) => Promise<Uint8Array>;
2235
+ }
2236
+ /**
2237
+ * Plugin contract for a resource kind: concentrates the per-kind domain
2238
+ * surface (authored entry schema, flatten, pre-I/O normalize, drift
2239
+ * equality) behind one interface that the core `diff` and shell
2240
+ * `buildDesired` functions dispatch through at runtime. Composes with the
2241
+ * `ResourceDriver<K>` port, which stays the I/O half of the kind.
2242
+ *
2243
+ * @template K - The {@link ResourceKind} discriminator this module handles.
2244
+ *
2245
+ * @example
2246
+ *
2247
+ * ```ts
2248
+ * import { type } from "arktype";
2249
+ *
2250
+ * import { asResourceKey, asSha256Hex, type ResourceKindModule } from "@bedrock-rbx/core";
2251
+ *
2252
+ * const stubKind: ResourceKindModule<"gamePass"> = {
2253
+ * kind: "gamePass",
2254
+ * entrySchema: type({
2255
+ * description: "string",
2256
+ * icon: type({ "en-us": "string" }).onUndeclaredKey("reject"),
2257
+ * name: "string",
2258
+ * "price?": "number | undefined",
2259
+ * }),
2260
+ * flatten: () => [],
2261
+ * normalize: async (input) => ({
2262
+ * data: {
2263
+ * description: input.description,
2264
+ * icon: input.icon,
2265
+ * iconFileHashes: {
2266
+ * "en-us": asSha256Hex(
2267
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2268
+ * ),
2269
+ * },
2270
+ * key: input.key,
2271
+ * kind: "gamePass",
2272
+ * name: input.name,
2273
+ * price: input.price,
2274
+ * },
2275
+ * success: true,
2276
+ * }),
2277
+ * fieldsEqual: (desired, current) => desired.name === current.name,
2278
+ * };
2279
+ *
2280
+ * expect(stubKind.kind).toBe("gamePass");
2281
+ * ```
2282
+ */
2283
+ interface ResourceKindModule<K extends ResourceKind> {
2284
+ /**
2285
+ * Optional plan-time invariant check called by `validatePlan` for every
2286
+ * `(kind, key)` pair that exists on both sides. Surfaces kind-specific
2287
+ * rejections (e.g. Removing a developer-product icon, which the upstream
2288
+ * API has no documented unset path for) before `diff` runs and before
2289
+ * any apply-side driver I/O is attempted. Kinds without plan-level
2290
+ * invariants omit this hook.
2291
+ */
2292
+ readonly assertReconcilable?: (current: ResourceCurrentState<K>, desired: DesiredFor<K>) => Result$1<undefined, BuildDesiredError>;
2293
+ /** ArkType schema for the authored entry body of this kind. */
2294
+ readonly entrySchema: Type$1<ResourceEntryByKind[K]>;
2295
+ /**
2296
+ * Managed-field equality. Identity fields (`key`, `kind`, and kind-specific
2297
+ * inputs like `placeId` or `universeId`) are excluded by the
2298
+ * implementation; `diff` treats `true` as "no drift, emit noop".
2299
+ */
2300
+ fieldsEqual(desired: DesiredFor<K>, current: ResourceCurrentState<K>): boolean;
2301
+ /**
2302
+ * Project a resolved `Config` into a flat, tagged list of this kind's
2303
+ * pre-I/O inputs. Pure and infallible: validation and per-environment
2304
+ * overlay merging have already happened upstream, so every invariant
2305
+ * this function relies on is guaranteed by the input shape.
2306
+ */
2307
+ flatten(config: ResolvedConfig): ReadonlyArray<InputFor<K>>;
2308
+ /** Discriminator literal for this kind. */
2309
+ readonly kind: K;
2310
+ /**
2311
+ * Layer pre-I/O work (file reads, hashing) onto an input to produce a
2312
+ * branded desired-state record. Rejections from `io.readFile` are caught
2313
+ * and surfaced as `fileReadFailed`.
2314
+ */
2315
+ normalize(input: InputFor<K>, io: KindIo): Promise<Result$1<DesiredFor<K>, BuildDesiredError>>;
2316
+ }
2317
+ /**
2318
+ * Polymorphic dispatch table keyed by {@link ResourceKind}, mapping each
2319
+ * kind to the {@link ResourceKindModule} that handles its domain surface.
2320
+ * Adding a new kind to `ResourceKind` is a compile error at `KindRegistry`
2321
+ * until a matching entry is supplied, matching how `DriverRegistry`
2322
+ * enforces the same invariant on its I/O half.
2323
+ *
2324
+ * @example
2325
+ *
2326
+ * ```ts
2327
+ * import { defaultKindRegistry, type KindRegistry } from "@bedrock-rbx/core";
2328
+ *
2329
+ * const registry: KindRegistry = defaultKindRegistry;
2330
+ * expect(registry.gamePass.kind).toBe("gamePass");
2331
+ * ```
2332
+ */
2333
+ type KindRegistry = { [K in ResourceKind]: ResourceKindModule<K> };
2334
+ /**
2335
+ * Desired-state narrowed to a single resource kind.
2336
+ *
2337
+ * @template K - The resource-kind discriminator.
2338
+ */
2339
+ type DesiredFor<K extends ResourceKind> = Extract<ResourceDesiredState, {
2340
+ readonly kind: K;
2341
+ }>;
2342
+ /**
2343
+ * Flat pre-I/O input narrowed to a single resource kind.
2344
+ *
2345
+ * @template K - The resource-kind discriminator.
2346
+ */
2347
+ type InputFor<K extends ResourceKind> = Extract<ResourceDesiredInput, {
2348
+ readonly kind: K;
2349
+ }>;
2350
+ //#endregion
2351
+ //#region src/core/icons.d.ts
2352
+ /**
2353
+ * Cost-gate for icon re-uploads. Returns `true` when the locally-hashed
2354
+ * desired icon differs from the hash recorded on the prior current-state
2355
+ * entry, signalling that the driver must re-upload before reconciling.
2356
+ * Returns `false` when the hashes match (no re-upload needed) and when
2357
+ * both sides report no icon.
2358
+ *
2359
+ * The signature takes hash maps directly (not whole-state) so the helper
2360
+ * is independent of any specific resource-kind shape; every icon-bearing
2361
+ * driver projects its own `iconFileHashes` and `outputs.iconFileHashes`
2362
+ * fields before calling.
2363
+ *
2364
+ * @param currentHashes - Hashes recorded on the prior current-state entry.
2365
+ * @param desiredHashes - Hashes layered onto the desired-state entry by
2366
+ * `normalize` from the local icon file's bytes.
2367
+ * @returns `true` when the driver should re-upload the icon.
2368
+ *
2369
+ * @example
2370
+ *
2371
+ * ```ts
2372
+ * import { asSha256Hex, shouldReuploadIcon } from "@bedrock-rbx/core";
2373
+ *
2374
+ * const previous = asSha256Hex(
2375
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2376
+ * );
2377
+ * const fresh = asSha256Hex(
2378
+ * "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
2379
+ * );
2380
+ *
2381
+ * expect(shouldReuploadIcon({ "en-us": previous }, { "en-us": previous })).toBe(false);
2382
+ * expect(shouldReuploadIcon({ "en-us": previous }, { "en-us": fresh })).toBe(true);
2383
+ * ```
2384
+ */
2385
+ declare function shouldReuploadIcon(currentHashes: Record<"en-us", Sha256Hex> | undefined, desiredHashes: Record<"en-us", Sha256Hex> | undefined): boolean;
2386
+ //#endregion
2387
+ //#region src/core/kinds/index.d.ts
2388
+ /**
2389
+ * Default {@link KindRegistry} composing every resource kind bedrock ships
2390
+ * out of the box. Iteration order (`gamePass`, `place`, `universe`,
2391
+ * `developerProduct`) matches the order `flattenConfig` emits entries
2392
+ * today, preserving the observable order of generated operations.
2393
+ *
2394
+ * @example
2395
+ *
2396
+ * ```ts
2397
+ * import { defaultKindRegistry } from "@bedrock-rbx/core";
2398
+ *
2399
+ * expect(defaultKindRegistry.gamePass.kind).toBe("gamePass");
2400
+ * expect(defaultKindRegistry.place.kind).toBe("place");
2401
+ * expect(defaultKindRegistry.universe.kind).toBe("universe");
2402
+ * expect(defaultKindRegistry.developerProduct.kind).toBe("developerProduct");
2403
+ * ```
2404
+ */
2405
+ declare const defaultKindRegistry: KindRegistry;
2406
+ //#endregion
2407
+ //#region src/core/migrate/migration-report.d.ts
2408
+ /**
2409
+ * Per-environment in-memory state snapshot map keyed by environment name.
2410
+ *
2411
+ * `Record` rather than the PRD-suggested `Map` so the field survives
2412
+ * `JSON.stringify` for downstream logging and parallel-iterates cleanly
2413
+ * with `Config.environments` (which is itself a `Record`).
2414
+ */
2415
+ type StatesByEnvironment = Readonly<Record<string, BedrockState>>;
2416
+ /**
2417
+ * Aggregate counts for the four `MigrationWarning` kinds. Computed by
2418
+ * folding `MigrationReport.warnings`; lets a CI gate skim totals without
2419
+ * iterating every entry. All fields are zero on a clean migration.
2420
+ */
2421
+ interface MigrationSummary {
2422
+ /** Number of `ambiguous` warnings emitted. */
2423
+ readonly ambiguousCount: number;
2424
+ /** Number of `blocked` warnings emitted. */
2425
+ readonly blockedCount: number;
2426
+ /** Number of `deferred` warnings emitted. */
2427
+ readonly deferredCount: number;
2428
+ /** Number of `interpretive` warnings emitted. */
2429
+ readonly interpretiveCount: number;
2430
+ }
2431
+ /**
2432
+ * Discriminated union describing one observation the migrator made about a
2433
+ * Mantle field that did not flow straight into bedrock config or state.
2434
+ *
2435
+ * - `deferred` - bedrock plans to support the field once the matching
2436
+ * resource kind ships; the migration is non-destructive.
2437
+ * - `blocked` - no Open Cloud writable endpoint exists; Mantle was using a
2438
+ * cookie or legacy API that bedrock cannot call.
2439
+ * - `interpretive` - the migrator applied a documented mapping rule
2440
+ * (cross-field fold, list-to-flag rewrite, URL-domain dispatch). Each
2441
+ * rule names the bedrock-side path it produced and the rule it followed
2442
+ * so the user can audit.
2443
+ * - `ambiguous` - the field is mappable but unsafe to act on without
2444
+ * user input; the migrator carries the hint forward instead of guessing.
2445
+ *
2446
+ * Every variant carries `mantlePath` rooted at the environment so the
2447
+ * report is searchable (for example
2448
+ * `production.experienceConfiguration_singleton.genre`).
2449
+ */
2450
+ type MigrationWarning = {
2451
+ readonly bedrockPath: string;
2452
+ readonly kind: "interpretive";
2453
+ readonly mantlePath: string;
2454
+ readonly rule: string;
2455
+ } | {
2456
+ readonly hint: string;
2457
+ readonly kind: "ambiguous";
2458
+ readonly mantlePath: string;
2459
+ } | {
2460
+ readonly kind: "blocked";
2461
+ readonly mantlePath: string;
2462
+ readonly reason: string;
2463
+ } | {
2464
+ readonly kind: "deferred";
2465
+ readonly mantlePath: string;
2466
+ readonly reason: string;
2467
+ };
2468
+ /**
2469
+ * Failure surfaced by `migrateMantleState`. Plain-data discriminated
2470
+ * union; narrow on `kind` rather than using `instanceof`.
2471
+ *
2472
+ * - `stateFileNotFound` - `deps.readFile` threw with `code: "ENOENT"`;
2473
+ * the file does not exist at the supplied path. Permission failures
2474
+ * (`EACCES`, `EPERM`) and other I/O errors are re-thrown rather than
2475
+ * wrapped here, so callers see the original code on the rejection.
2476
+ * - `stateParseFailed` - the YAML parser refused the file's contents.
2477
+ * - `unsupportedMantleStateVersion` - the parsed file's `version` field is
2478
+ * not one of the values in `supported`. V0.1 supports `"6"` only; older
2479
+ * versions need to be upgraded with any recent Mantle release first.
2480
+ * - `primaryEnvironmentRequired` - the input has more than one environment
2481
+ * and `deps.primaryEnvironment` was not supplied. The migrator refuses
2482
+ * to silently pick a winner.
2483
+ * - `primaryEnvironmentNotFound` - `deps.primaryEnvironment` does not match
2484
+ * any environment in the input.
2485
+ * - `internalError` - the migrator's own emitted config failed
2486
+ * `validateConfig`; `cause` carries the `ConfigError` so callers can
2487
+ * inspect each `validationFailed` issue. Defensive bug catcher that
2488
+ * callers should never see in practice.
2489
+ */
2490
+ type MigrateError = {
2491
+ readonly available: ReadonlyArray<string>;
2492
+ readonly kind: "primaryEnvironmentNotFound";
2493
+ readonly primary: string;
2494
+ } | {
2495
+ readonly available: ReadonlyArray<string>;
2496
+ readonly kind: "primaryEnvironmentRequired";
2497
+ } | {
2498
+ readonly cause: ConfigError;
2499
+ readonly kind: "internalError";
2500
+ readonly reason: string;
2501
+ } | {
2502
+ readonly found: string;
2503
+ readonly kind: "unsupportedMantleStateVersion";
2504
+ readonly supported: ReadonlyArray<string>;
2505
+ } | {
2506
+ readonly kind: "stateFileNotFound";
2507
+ readonly path: string;
2508
+ } | {
2509
+ readonly kind: "stateParseFailed";
2510
+ readonly path: string;
2511
+ readonly reason: string;
2512
+ };
2513
+ /**
2514
+ * Result returned by a successful `migrateMantleState` call.
2515
+ *
2516
+ * `config` is the bedrock-shape projection of the Mantle state file,
2517
+ * already validated against the runtime schema (a failure to validate
2518
+ * surfaces as `MigrateError.internalError`, not as a returned report).
2519
+ *
2520
+ * `configFileContent` is the same data rendered as TypeScript source
2521
+ * (`defineConfig({...})`) so the caller can write it straight to disk
2522
+ * without re-serializing. `loadConfig` round-trips it cleanly.
2523
+ *
2524
+ * `statesByEnvironment` carries one in-memory `BedrockState` per
2525
+ * environment from the input. Truthful per environment (no factorization)
2526
+ * so `bedrock deploy --env=<env>` produces zero ops on first run.
2527
+ *
2528
+ * `warnings` and `summary` describe what the migrator did *not* migrate
2529
+ * verbatim, classified for triage. The skeleton emits no warnings.
2530
+ */
2531
+ interface MigrationReport {
2532
+ /** Validated bedrock config built from the Mantle state file. */
2533
+ readonly config: Config;
2534
+ /** Same `config` rendered as TypeScript source the caller can write to disk. */
2535
+ readonly configFileContent: string;
2536
+ /** One `BedrockState` per environment in the input, keyed by environment name. */
2537
+ readonly statesByEnvironment: StatesByEnvironment;
2538
+ /** Aggregate counts of `warnings` by kind. */
2539
+ readonly summary: MigrationSummary;
2540
+ /** One entry per non-trivial mapping or skipped Mantle field. */
2541
+ readonly warnings: ReadonlyArray<MigrationWarning>;
2542
+ }
2543
+ //#endregion
2544
+ //#region src/core/resolve-state-config.d.ts
2545
+ /**
2546
+ * Failure surfaced when no `StateConfig` is configured for the requested
2547
+ * environment. The shell layer wraps this in a `DeployError` when default
2548
+ * state-port construction is requested but the project has not declared
2549
+ * where state should live.
2550
+ */
2551
+ interface StateNotConfiguredError {
2552
+ /** Environment that the resolver was called against. */
2553
+ readonly environment: string;
2554
+ /** Literal discriminator for narrowing. */
2555
+ readonly kind: "stateNotConfigured";
2556
+ }
2557
+ /**
2558
+ * Minimal structural input the state resolver needs. Both `Config`
2559
+ * (pre-merge, discriminated XOR union) and `ResolvedConfig` (post-merge)
2560
+ * satisfy this shape, so callers can route either in without coupling
2561
+ * the resolver to the discriminated-union arms.
2562
+ */
2563
+ interface StateResolutionInputs {
2564
+ readonly environments: Record<string, undefined | {
2565
+ readonly state?: StateConfig;
2566
+ }>;
2567
+ readonly state?: StateConfig;
2568
+ }
2569
+ /**
2570
+ * Pick the `StateConfig` that applies to `environment`. Per-environment
2571
+ * overrides win over the root block; if neither is present, returns
2572
+ * `Err(stateNotConfigured)` so the deploy boundary can surface a typed
2573
+ * error instead of silently falling back.
2574
+ *
2575
+ * @param config - Validated project config.
2576
+ * @param environment - Target environment name.
2577
+ * @returns The resolved `StateConfig`, or `Err(stateNotConfigured)` when
2578
+ * neither the environment override nor the root block is set.
2579
+ * @example
2580
+ *
2581
+ * ```ts
2582
+ * import { resolveStateConfig } from "@bedrock-rbx/core";
2583
+ *
2584
+ * const result = resolveStateConfig(
2585
+ * {
2586
+ * state: { backend: "gist", gistId: "root-gist" },
2587
+ * environments: {
2588
+ * production: { state: { backend: "gist", gistId: "prod-gist" } },
2589
+ * },
2590
+ * },
2591
+ * "production",
2592
+ * );
2593
+ *
2594
+ * expect(result.success).toBeTrue();
2595
+ * if (result.success) {
2596
+ * expect(result.data).toContainEntry(["gistId", "prod-gist"]);
2597
+ * }
2598
+ * ```
2599
+ */
2600
+ declare function resolveStateConfig(config: StateResolutionInputs, environment: string): Result$1<StateConfig, StateNotConfiguredError>;
2601
+ //#endregion
2602
+ //#region src/core/select-environment.d.ts
2603
+ /**
2604
+ * Failure surfaced when `selectEnvironment` is asked for an environment
2605
+ * name that is not a key of `config.environments`. Carries the list of
2606
+ * declared names so callers can render a "did you mean?" hint or a
2607
+ * close-match suggestion.
2608
+ */
2609
+ interface UnknownEnvironmentError {
2610
+ /** Environment names that the config actually declared. */
2611
+ readonly declared: ReadonlyArray<string>;
2612
+ /** Environment name the caller asked for. */
2613
+ readonly environment: string;
2614
+ /** Literal discriminator for narrowing. */
2615
+ readonly kind: "unknownEnvironment";
2616
+ }
2617
+ /**
2618
+ * Failure surfaced when a merged place entry is missing a required field.
2619
+ * Two paths reach this error: a root place declared without a matching
2620
+ * per-environment overlay supplying `placeId`, and an overlay-only place
2621
+ * declared under `environments.X.places` with no matching root entry to
2622
+ * supply `filePath`. Surfacing both at the resolution boundary attributes
2623
+ * the missing field to the offending entry's key instead of letting
2624
+ * `buildDesired` crash with a generic `fileReadFailed` later on.
2625
+ */
2626
+ interface IncompletePlaceEntryError {
2627
+ /** ResourceKey of the place entry that is missing a required field. */
2628
+ readonly key: string;
2629
+ /** Environment whose overlay was projected onto the config. */
2630
+ readonly environment: string;
2631
+ /** Literal discriminator for narrowing. */
2632
+ readonly kind: "incompletePlaceEntry";
2633
+ /** Field that the merged entry lacks. */
2634
+ readonly missingField: "filePath" | "placeId";
2635
+ }
2636
+ /**
2637
+ * Failure surfaced when a merged `universe` block lacks `universeId`.
2638
+ * The schema-level XOR rule normally prevents this by requiring
2639
+ * `universeId` either at the root or on every per-environment overlay;
2640
+ * this error remains as a typed safety net for callers that bypass
2641
+ * `validateConfig` and hand a `Config` to `selectEnvironment` directly.
2642
+ */
2643
+ interface IncompleteUniverseEntryError {
2644
+ /** Environment whose overlay was projected onto the config. */
2645
+ readonly environment: string;
2646
+ /** Literal discriminator for narrowing. */
2647
+ readonly kind: "incompleteUniverseEntry";
2648
+ /** Field that the merged entry lacks. V1 only surfaces `"universeId"`. */
2649
+ readonly missingField: "universeId";
2650
+ }
2651
+ /** Failure modes returned by {@link selectEnvironment}. */
2652
+ type SelectEnvironmentError = IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
2653
+ /**
2654
+ * Project a validated `Config` onto a single environment. Looks up the
2655
+ * matching `environments[environment]` entry, deep-merges its resource
2656
+ * overlay (`passes`, `places`, `universe`) over the root config via defu,
2657
+ * and applies the env-level state override when present (the env entry's
2658
+ * `state` field wins; otherwise the root `state` flows through).
2659
+ *
2660
+ * Pure: no I/O. Returns a `ResolvedConfig` ready to feed into downstream
2661
+ * functions (`flattenConfig`, `buildDefaultRegistry`, `resolveStateConfig`).
2662
+ * The post-merge view promotes `places` from `Record<string, PlaceEntry>`
2663
+ * (root: file-paths only) to `Record<string, ResolvedPlaceEntry>` (root +
2664
+ * overlay merged). `environments` and `extends` are passed through
2665
+ * unchanged because they preserve the shape relationship to `Config`;
2666
+ * downstream consumers do not read them post-merge.
2667
+ *
2668
+ * Defu's merge semantics are deliberate: keyed-map collections merge by
2669
+ * key (so a place declared in both root and overlay produces a single
2670
+ * entry whose overlay-supplied fields win), and `null` / `undefined` in
2671
+ * the overlay are skipped (so the overlay never deletes a root field).
2672
+ * State has its own resolution path (a single replacement, not a
2673
+ * deep-merge) because it is a tagged union: a deep-merge of
2674
+ * `{ backend: "s3" }` over `{ backend: "gist", gistId }` would produce
2675
+ * a malformed `{ backend: "s3", gistId }`.
2676
+ *
2677
+ * State is left absent when neither the env override nor the root block
2678
+ * provides one. Callers that require a resolved `StateConfig` should
2679
+ * route through `resolveStateConfig` or `buildStatePort`; the absent
2680
+ * case surfaces as a typed `stateNotConfigured` there.
2681
+ *
2682
+ * Limitation in v1: a per-environment universe overlay that introduces a
2683
+ * brand-new universe block may still have optional fields missing, since
2684
+ * the overlay type only requires the identity-bearing key. The resolver
2685
+ * surfaces the entry as-is; the universe driver reports the missing
2686
+ * field when it tries to consume the entry. Universe is a singleton with
2687
+ * 20+ optional fields, so the same `incompletePlaceEntry`-style validation
2688
+ * is deferred to a separate follow-up.
2689
+ *
2690
+ * When the project sets a `displayNamePrefix` (or omits it, in which case
2691
+ * prefixing defaults to enabled) and the chosen environment declares a
2692
+ * non-empty `label`, the resolver renders the configured template via
2693
+ * `renderDisplayNamePrefix` and prepends the result to `universe.displayName`
2694
+ * and every declared place `displayName`. An undeclared `displayName`, an
2695
+ * empty/absent label, or an explicit `displayNamePrefix.enabled: false` all
2696
+ * skip prefixing for the affected fields.
2697
+ *
2698
+ * @example
2699
+ *
2700
+ * ```ts
2701
+ * import { selectEnvironment } from "@bedrock-rbx/core";
2702
+ * import type { Config } from "@bedrock-rbx/core/config";
2703
+ *
2704
+ * const config: Config = {
2705
+ * environments: {
2706
+ * production: { universe: { universeId: "999" } },
2707
+ * },
2708
+ * state: { backend: "gist", gistId: "abc123" },
2709
+ * universe: { voiceChatEnabled: true },
2710
+ * };
2711
+ *
2712
+ * const result = selectEnvironment(config, "production");
2713
+ *
2714
+ * expect(result.success).toBeTrue();
2715
+ * if (result.success) {
2716
+ * expect(result.data.universe?.universeId).toBe("999");
2717
+ * expect(result.data.universe?.voiceChatEnabled).toBeTrue();
2718
+ * expect(result.data.state?.backend).toBe("gist");
2719
+ * }
2720
+ * ```
2721
+ *
2722
+ * @param config - Validated project config carrying at least one
2723
+ * environment under `environments`.
2724
+ * @param environment - Environment name to project onto. Must be a key
2725
+ * of `config.environments`.
2726
+ * @returns `Ok(ResolvedConfig)` with the merged resource fields and the
2727
+ * resolved state, or `Err(SelectEnvironmentError)` describing why the
2728
+ * projection failed.
2729
+ */
2730
+ declare function selectEnvironment(config: Config, environment: string): Result$1<ResolvedConfig, SelectEnvironmentError>;
2731
+ //#endregion
2732
+ //#region src/core/state-file.d.ts
2733
+ /**
2734
+ * Serialize a {@link BedrockState} to the on-disk JSON representation used by
2735
+ * state-port adapters.
2736
+ *
2737
+ * The on-disk shape wraps the in-memory state with a
2738
+ * `$bedrock: { version: N }` envelope so that a future breaking change to the
2739
+ * schema can be detected and rejected at parse time rather than silently
2740
+ * accepted. The top-level `version` field is not duplicated on disk.
2741
+ *
2742
+ * @example
2743
+ *
2744
+ * ```ts
2745
+ * import { serializeStateFile, type BedrockState } from "@bedrock-rbx/core";
2746
+ *
2747
+ * const state: BedrockState = {
2748
+ * environment: "production",
2749
+ * resources: [],
2750
+ * version: 1,
2751
+ * };
2752
+ *
2753
+ * const wire = serializeStateFile(state);
2754
+ * expect(JSON.parse(wire)).toStrictEqual({
2755
+ * $bedrock: { version: 1 },
2756
+ * environment: "production",
2757
+ * resources: [],
2758
+ * });
2759
+ * ```
2760
+ *
2761
+ * @param state - The in-memory state snapshot to serialize.
2762
+ * @returns A pretty-printed JSON string ready to hand to a state adapter's write method.
2763
+ */
2764
+ declare function serializeStateFile(state: BedrockState): string;
2765
+ /**
2766
+ * Parse a raw on-disk state file into a {@link BedrockState}.
2767
+ *
2768
+ * A backend that reports "no state file for this environment yet" must pass
2769
+ * `undefined`: that distinguishes a legitimate first deploy from a file that
2770
+ * exists but cannot be trusted.
2771
+ *
2772
+ * @example
2773
+ *
2774
+ * ```ts
2775
+ * import { parseStateFile } from "@bedrock-rbx/core";
2776
+ *
2777
+ * const freshStart = parseStateFile(undefined, "gist:abc123/state.production.json");
2778
+ * expect(freshStart.success).toBeTrue();
2779
+ * if (freshStart.success) {
2780
+ * expect(freshStart.data).toBeUndefined();
2781
+ * }
2782
+ * ```
2783
+ *
2784
+ * @param raw - Raw file contents as a string, or `undefined` when the
2785
+ * backend reports no file exists yet.
2786
+ * @param file - Adapter-specific identifier included in any `StateError`
2787
+ * surfaced during parsing.
2788
+ * @returns `Ok(undefined)` for a missing file, `Ok(state)` for a parseable
2789
+ * file, or `Err(StateError)` for anything that cannot be trusted.
2790
+ */
2791
+ declare function parseStateFile(raw: string | undefined, file: string): Result$1<BedrockState | undefined, StateError>;
2792
+ //#endregion
2793
+ //#region src/core/validate-plan.d.ts
2794
+ /**
2795
+ * Plan-time invariant check that runs after `buildDesired` and before
2796
+ * `diff`. Walks paired `(kind, key)` entries and dispatches to each
2797
+ * kind module's optional `assertReconcilable` hook so kind-specific
2798
+ * rejections (e.g. Removing a developer-product icon, which the upstream
2799
+ * API has no documented unset path for) surface as typed errors before
2800
+ * `diff` runs and before any apply-side driver I/O is attempted.
2801
+ *
2802
+ * Pure and synchronous. Current-only entries (no matching desired) are
2803
+ * ignored: their reconciliation is `diff`'s concern, not this seam's.
2804
+ *
2805
+ * @param desired - Desired state from `buildDesired`.
2806
+ * @param current - Prior current state from the state port.
2807
+ * @returns `Ok(undefined)` when every paired entry passes its kind-level
2808
+ * reconcilability check, or the first `Err` returned by a hook.
2809
+ *
2810
+ * @example
2811
+ *
2812
+ * ```ts
2813
+ * import { asResourceKey, asRobloxAssetId, asSha256Hex, validatePlan } from "@bedrock-rbx/core";
2814
+ *
2815
+ * const result = validatePlan(
2816
+ * [
2817
+ * {
2818
+ * description: "Stocks the player up with 1,000 premium gems.",
2819
+ * isRegionalPricingEnabled: undefined,
2820
+ * key: asResourceKey("gem-pack"),
2821
+ * kind: "developerProduct",
2822
+ * name: "Gem Pack",
2823
+ * price: undefined,
2824
+ * storePageEnabled: undefined,
2825
+ * },
2826
+ * ],
2827
+ * [
2828
+ * {
2829
+ * description: "Stocks the player up with 1,000 premium gems.",
2830
+ * icon: { "en-us": "assets/gem-pack.png" },
2831
+ * iconFileHashes: {
2832
+ * "en-us": asSha256Hex(
2833
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2834
+ * ),
2835
+ * },
2836
+ * isRegionalPricingEnabled: undefined,
2837
+ * key: asResourceKey("gem-pack"),
2838
+ * kind: "developerProduct",
2839
+ * name: "Gem Pack",
2840
+ * outputs: { productId: asRobloxAssetId("9876543210") },
2841
+ * price: undefined,
2842
+ * storePageEnabled: undefined,
2843
+ * },
2844
+ * ],
2845
+ * );
2846
+ *
2847
+ * expect(result.success).toBeFalse();
2848
+ * if (!result.success) {
2849
+ * expect(result.err.kind).toBe("iconRemovalRejected");
2850
+ * }
2851
+ * ```
2852
+ */
2853
+ declare function validatePlan(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): Result$1<undefined, BuildDesiredError>;
2854
+ //#endregion
2855
+ //#region src/shell/apply-ops.d.ts
2856
+ /**
2857
+ * Failure surfaced by `applyOps` when an operation cannot be applied.
2858
+ * Plain-data discriminated union; narrow on `kind`, do not `instanceof` it.
2859
+ *
2860
+ * `appliedSoFar` carries the driver outputs from operations that succeeded
2861
+ * before the failing one, in dispatched order. Callers persist this so a
2862
+ * follow-up reconcile does not duplicate Roblox-side resources that have
2863
+ * already been created or updated.
2864
+ *
2865
+ * @example
2866
+ *
2867
+ * ```ts
2868
+ * import { asResourceKey, type ApplyError } from "@bedrock-rbx/core";
2869
+ *
2870
+ * function describe(err: ApplyError): string {
2871
+ * switch (err.kind) {
2872
+ * case "driverFailure": {
2873
+ * return `driver failed for ${err.key}: ${err.cause.message}`;
2874
+ * }
2875
+ * case "updateUnsupported": {
2876
+ * return `update not supported for ${err.key}`;
2877
+ * }
2878
+ * }
2879
+ * }
2880
+ *
2881
+ * const err: ApplyError = {
2882
+ * key: asResourceKey("vip-pass"),
2883
+ * appliedSoFar: [],
2884
+ * kind: "updateUnsupported",
2885
+ * };
2886
+ *
2887
+ * expect(describe(err)).toBe("update not supported for vip-pass");
2888
+ * ```
2889
+ */
2890
+ type ApplyError = {
2891
+ readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
2892
+ readonly cause: OpenCloudError$1;
2893
+ readonly key: ResourceKey;
2894
+ readonly kind: "driverFailure";
2895
+ } | {
2896
+ readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
2897
+ readonly key: ResourceKey;
2898
+ readonly kind: "updateUnsupported";
2899
+ };
2900
+ /**
2901
+ * Dispatch each reconciliation operation to the matching resource driver
2902
+ * with first-fail semantics: on the first `Err` (driver failure or
2903
+ * `updateUnsupported`), the remaining operations are skipped and the error
2904
+ * is returned verbatim.
2905
+ *
2906
+ * Behaviour:
2907
+ * - `create` operations are routed to `registry[op.desired.kind].create`.
2908
+ * - `update` operations are routed to `registry[op.desired.kind].update`
2909
+ * when the driver exposes it; otherwise they short-circuit to an
2910
+ * `updateUnsupported` Err without invoking the driver.
2911
+ * - `noop` operations are skipped entirely (no I/O, no dispatch).
2912
+ *
2913
+ * On success the returned array carries the driver outputs for every
2914
+ * non-noop op, in dispatched order. Noops are not represented; callers
2915
+ * needing a full post-apply snapshot merge with the pre-apply current
2916
+ * state keyed by `ResourceKey`.
2917
+ *
2918
+ * @param ops - Reconciliation operations produced by `diff`, applied in order.
2919
+ * @param registry - Per-kind driver table; dispatch uses `op.desired.kind` as the index.
2920
+ * @returns `Ok(state)` when every operation succeeds, where `state` holds
2921
+ * driver outputs for each non-noop op in dispatched order; or the first
2922
+ * failure encountered.
2923
+ * @throws Whatever the dispatched driver rejects with outside its `Result`
2924
+ * return. A driver whose injected I/O (file reads, network calls, etc.)
2925
+ * throws will surface that rejection here rather than translating it into
2926
+ * a `Result` failure; wrap the call site in a try/catch when drivers are
2927
+ * not trusted to contain their own rejections.
2928
+ * @example
2929
+ *
2930
+ * ```ts
2931
+ * import {
2932
+ * applyOps,
2933
+ * asResourceKey,
2934
+ * asRobloxAssetId,
2935
+ * asSha256Hex,
2936
+ * type DriverRegistry,
2937
+ * type Operation,
2938
+ * } from "@bedrock-rbx/core";
2939
+ *
2940
+ * const registry: DriverRegistry = {
2941
+ * gamePass: {
2942
+ * async create(desired) {
2943
+ * return {
2944
+ * data: {
2945
+ * ...desired,
2946
+ * outputs: {
2947
+ * assetId: asRobloxAssetId("9876543210"),
2948
+ * iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
2949
+ * },
2950
+ * },
2951
+ * success: true,
2952
+ * };
2953
+ * },
2954
+ * },
2955
+ * place: {
2956
+ * async create(desired) {
2957
+ * return {
2958
+ * data: { ...desired, outputs: { versionNumber: 1 } },
2959
+ * success: true,
2960
+ * };
2961
+ * },
2962
+ * },
2963
+ * universe: {
2964
+ * async create(desired) {
2965
+ * return {
2966
+ * data: { ...desired, outputs: { rootPlaceId: asRobloxAssetId("4711") } },
2967
+ * success: true,
2968
+ * };
2969
+ * },
2970
+ * },
2971
+ * developerProduct: {
2972
+ * async create(desired) {
2973
+ * return {
2974
+ * data: {
2975
+ * ...desired,
2976
+ * outputs: { productId: asRobloxAssetId("8172635495") },
2977
+ * },
2978
+ * success: true,
2979
+ * };
2980
+ * },
2981
+ * },
2982
+ * };
2983
+ *
2984
+ * const ops: ReadonlyArray<Operation> = [
2985
+ * {
2986
+ * key: asResourceKey("vip-pass"),
2987
+ * type: "create",
2988
+ * desired: {
2989
+ * key: asResourceKey("vip-pass"),
2990
+ * name: "VIP Pass",
2991
+ * description: "Grants VIP perks.",
2992
+ * icon: { "en-us": "assets/vip-icon.png" },
2993
+ * iconFileHashes: {
2994
+ * "en-us": asSha256Hex(
2995
+ * "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2996
+ * ),
2997
+ * },
2998
+ * kind: "gamePass",
2999
+ * price: 500,
3000
+ * },
3001
+ * },
3002
+ * ];
3003
+ *
3004
+ * return applyOps(ops, registry).then((result) => {
3005
+ * expect(result.success).toBe(true);
3006
+ * expect(result.success && result.data).toHaveLength(1);
3007
+ * });
3008
+ * ```
3009
+ */
3010
+ declare function applyOps(ops: ReadonlyArray<Operation>, registry: DriverRegistry): Promise<Result$1<ReadonlyArray<ResourceCurrentState>, ApplyError>>;
3011
+ //#endregion
3012
+ //#region src/shell/build-state-port.d.ts
3013
+ /**
3014
+ * Failure surfaced when a default-constructed adapter cannot find a
3015
+ * required environment variable. The deploy boundary wraps this in a
3016
+ * `DeployError` so the caller sees a typed Result instead of an
3017
+ * exception or a confusing downstream HTTP error.
3018
+ */
3019
+ interface MissingCredentialError {
3020
+ /** Literal discriminator for narrowing. */
3021
+ readonly kind: "missingCredential";
3022
+ /** Whether the credential was needed for the state backend or the driver registry. */
3023
+ readonly purpose: "registry" | "stateBackend";
3024
+ /** Environment variable name the default-construction path tried to read. */
3025
+ readonly variable: string;
3026
+ }
3027
+ /**
3028
+ * Failure surfaced when the dispatch helper sees a `state.backend` value
3029
+ * it does not recognize. The hint points at `opts.statePort` so the
3030
+ * caller can pass a custom adapter as an escape hatch.
3031
+ */
3032
+ interface UnsupportedBackendError {
3033
+ /** Backend name read from `state.backend`. */
3034
+ readonly backend: string;
3035
+ /** Suggested escape hatch routed back to the caller. */
3036
+ readonly hint: string;
3037
+ /** Literal discriminator for narrowing. */
3038
+ readonly kind: "unsupportedBackend";
3039
+ }
3040
+ /** Inputs for {@link buildStatePort}. */
3041
+ interface BuildStatePortDeps {
3042
+ /** Optional `fetch` seam plumbed through to the gist adapter for tests. */
3043
+ readonly fetch?: GistFetch | undefined;
3044
+ /** Reads an environment variable; injected so tests stay free of `process.env`. */
3045
+ readonly getEnv: (name: string) => string | undefined;
3046
+ /** Resolved state configuration for the target environment. */
3047
+ readonly stateConfig: StateConfig;
3048
+ }
3049
+ /**
3050
+ * Construct a `StatePort` from a resolved `StateConfig`. Dispatches on
3051
+ * `stateConfig.backend` to the matching builtin adapter; reads the
3052
+ * required credential from `getEnv` and surfaces `missingCredential` or
3053
+ * `unsupportedBackend` as typed Results.
3054
+ *
3055
+ * @example
3056
+ *
3057
+ * ```ts
3058
+ * import { buildStatePort } from "@bedrock-rbx/core";
3059
+ *
3060
+ * const port = buildStatePort({
3061
+ * fetch: async () =>
3062
+ * new Response(JSON.stringify({ files: {} }), { status: 200 }),
3063
+ * getEnv: (name) => (name === "GITHUB_TOKEN" ? "ghp_example" : undefined),
3064
+ * stateConfig: { backend: "gist", gistId: "abc123" },
3065
+ * });
3066
+ *
3067
+ * expect(port.success).toBeTrue();
3068
+ * ```
3069
+ *
3070
+ * @param deps - Resolved state config plus credential-injection seams.
3071
+ * @returns A `StatePort` on success, or a typed Err describing the
3072
+ * missing credential or the unsupported backend.
3073
+ */
3074
+ declare function buildStatePort(deps: BuildStatePortDeps): Result$1<StatePort, MissingCredentialError | UnsupportedBackendError>;
3075
+ //#endregion
3076
+ //#region src/shell/build-default-registry.d.ts
3077
+ /**
3078
+ * Failure surfaced when default-constructing a registry needs a config
3079
+ * field that is not present. The deploy boundary wraps this in a
3080
+ * `DeployError` so the caller sees a typed Result instead of a downstream
3081
+ * driver error.
3082
+ */
3083
+ interface RegistryConfigError {
3084
+ /** Suggested fix routed back to the caller. */
3085
+ readonly hint: string;
3086
+ /** Literal discriminator for narrowing. */
3087
+ readonly kind: "registryConfigMissing";
3088
+ /** Which config field was missing. */
3089
+ readonly missing: "universeId";
3090
+ }
3091
+ /** Inputs for {@link buildDefaultRegistry}. */
3092
+ interface BuildDefaultRegistryDeps {
3093
+ /** Resolved project config; supplies `universe.universeId` and is read for nothing else. */
3094
+ readonly config: ResolvedConfig;
3095
+ /** Reads an environment variable; injected so tests stay free of `process.env`. */
3096
+ readonly getEnv: (name: string) => string | undefined;
3097
+ /** Reader plumbed into kind-specific drivers that ingest file bytes. */
3098
+ readonly readFile: (path: string) => Promise<Uint8Array>;
3099
+ }
3100
+ /**
3101
+ * Construct the default `DriverRegistry` from `config.universe.universeId`
3102
+ * and `ROBLOX_API_KEY`. Reads the API key via the injected `getEnv` seam
3103
+ * and surfaces `missingCredential` or `registryConfigMissing` as typed
3104
+ * Results instead of throwing.
3105
+ *
3106
+ * @example
3107
+ *
3108
+ * ```ts
3109
+ * import { buildDefaultRegistry } from "@bedrock-rbx/core";
3110
+ *
3111
+ * const registry = buildDefaultRegistry({
3112
+ * config: {
3113
+ * environments: { production: {} },
3114
+ * state: { backend: "gist", gistId: "abc" },
3115
+ * universe: { universeId: "1234567890" },
3116
+ * },
3117
+ * getEnv: () => "rbx-test",
3118
+ * readFile: async () => new Uint8Array(),
3119
+ * });
3120
+ *
3121
+ * expect(registry.success).toBeTrue();
3122
+ * ```
3123
+ *
3124
+ * @param deps - Validated config plus credential and file-reader seams.
3125
+ * @returns A `DriverRegistry` on success, or a typed Err describing the
3126
+ * missing API key or the missing universe declaration.
3127
+ */
3128
+ declare function buildDefaultRegistry(deps: BuildDefaultRegistryDeps): Result$1<DriverRegistry, MissingCredentialError | RegistryConfigError>;
3129
+ //#endregion
3130
+ //#region src/shell/build-desired.d.ts
3131
+ /**
3132
+ * Layer file I/O onto a flat tagged list of resource inputs to produce
3133
+ * `ResourceDesiredState`.
3134
+ *
3135
+ * For each input, reads the file bytes via the injected `readFile`, computes
3136
+ * the SHA-256 hex digest, and assembles the branded desired-state record
3137
+ * that `diff` consumes. Entries are processed sequentially so the first
3138
+ * failure's attribution is deterministic.
3139
+ *
3140
+ * @param inputs - Flat tagged resource inputs from `flattenConfig`.
3141
+ * @param readFile - Reads file bytes for a given path; rejection becomes a
3142
+ * `fileReadFailed` Err.
3143
+ * @returns `Ok` with the desired-state array (same length and order as
3144
+ * `inputs`), or `Err` with the first I/O failure.
3145
+ * @example
3146
+ *
3147
+ * ```ts
3148
+ * import { asResourceKey, buildDesired } from "@bedrock-rbx/core";
3149
+ *
3150
+ * async function readFile(): Promise<Uint8Array> {
3151
+ * return new Uint8Array([1, 2, 3]);
3152
+ * }
3153
+ *
3154
+ * return buildDesired(
3155
+ * [
3156
+ * {
3157
+ * description: "Grants VIP perks.",
3158
+ * icon: { "en-us": "assets/vip-icon.png" },
3159
+ * key: asResourceKey("vip-pass"),
3160
+ * kind: "gamePass",
3161
+ * name: "VIP Pass",
3162
+ * price: 500,
3163
+ * },
3164
+ * ],
3165
+ * readFile,
3166
+ * ).then((result) => {
3167
+ * expect(result.success).toBeTrue();
3168
+ * if (result.success) {
3169
+ * expect(result.data).toHaveLength(1);
3170
+ * expect(result.data[0]!.kind).toBe("gamePass");
3171
+ * }
3172
+ * });
3173
+ * ```
3174
+ */
3175
+ declare function buildDesired(inputs: ReadonlyArray<ResourceDesiredInput>, readFile: (path: string) => Promise<Uint8Array>): Promise<Result$1<ReadonlyArray<ResourceDesiredState>, BuildDesiredError>>;
3176
+ //#endregion
3177
+ //#region src/shell/load-config.d.ts
3178
+ /**
3179
+ * Options for {@link loadConfig}. Matches a subset of c12's loader options;
3180
+ * additional fields land with the issues that introduce each flow.
3181
+ */
3182
+ interface LoadConfigOptions {
3183
+ /**
3184
+ * Path to a specific config file to load, including its extension.
3185
+ * Resolved relative to `cwd` when not absolute. Loaded as-is with no
3186
+ * extension search; if the file does not exist at the given path,
3187
+ * `loadConfig` returns `fileNotFound`. When omitted, `loadConfig`
3188
+ * discovers `bedrock.config.{ts,js,...}` from `cwd`.
3189
+ */
3190
+ readonly configFile?: string;
3191
+ /**
3192
+ * Directory to search from. Defaults to `process.cwd()` at call time, so
3193
+ * each invocation sees the current working directory.
3194
+ */
3195
+ readonly cwd?: string;
3196
+ }
3197
+ /**
3198
+ * Discover, parse, and validate the project config.
3199
+ *
3200
+ * Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json}`, `.bedrockrc*`,
3201
+ * and `package.json#bedrock` starting at `options.cwd` (or the current
3202
+ * working directory). Returns a fresh, mutable `Config` on every call so
3203
+ * long-running scripts see up-to-date values.
3204
+ *
3205
+ * When the exported default is a function (sync or async), `loadConfig`
3206
+ * invokes it with an empty `ConfigContext` and awaits the result before
3207
+ * validating.
3208
+ *
3209
+ * Errors return via `Result`:
3210
+ * - `fileNotFound` - no config file was discovered under the search path.
3211
+ * - `parseFailed` - a config file was found but could not be parsed (for
3212
+ * example, malformed YAML or JSON).
3213
+ * - `validationFailed` - a file was found and parsed, but its content did
3214
+ * not satisfy the runtime schema.
3215
+ * - `configFunctionFailed` - a function-form config threw or its returned
3216
+ * promise rejected while being invoked.
3217
+ *
3218
+ * @param options - Loader options.
3219
+ * @returns `Ok` with the validated `Config`, or `Err` with a `ConfigError`.
3220
+ * @example
3221
+ *
3222
+ * ```ts
3223
+ * import { loadConfig } from "@bedrock-rbx/core";
3224
+ *
3225
+ * return loadConfig({
3226
+ * configFile: "bedrock.staging.config.yaml",
3227
+ * cwd: "/path/that/does/not/have/a/config",
3228
+ * }).then((result) => {
3229
+ * expect(result.success).toBeFalse();
3230
+ * if (!result.success) {
3231
+ * expect(result.err.kind).toBe("fileNotFound");
3232
+ * }
3233
+ * });
3234
+ * ```
3235
+ */
3236
+ declare function loadConfig(options?: LoadConfigOptions): Promise<Result$1<Config, ConfigError>>;
3237
+ //#endregion
3238
+ //#region src/shell/deploy.d.ts
3239
+ /**
3240
+ * Inputs for `deploy`. Every field except `environment` is optional;
3241
+ * omitted dependencies are default-constructed from the project config
3242
+ * and the environment variables `GITHUB_TOKEN` and `ROBLOX_API_KEY`.
3243
+ */
3244
+ interface DeployOptions {
3245
+ /** Pre-loaded, optionally-mutated project config. Omit to call `loadConfig()` automatically. */
3246
+ readonly config?: Config;
3247
+ /** Environment name; threaded into `StatePort.read` and the persisted snapshot. */
3248
+ readonly environment: string;
3249
+ /** `fetch` override plumbed into the default-constructed gist adapter when `statePort` is omitted. */
3250
+ readonly fetch?: GistFetch;
3251
+ /** Reads an environment variable; defaults to `(name) => process.env[name]`. */
3252
+ readonly getEnv?: (name: string) => string | undefined;
3253
+ /** Loader invoked when `config` is omitted; defaults to `loadConfig` from this package. */
3254
+ readonly loadConfig?: (options?: LoadConfigOptions) => Promise<Result$1<Config, ConfigError>>;
3255
+ /** Reads file bytes for resources that have file-backed inputs. Defaults to `node:fs/promises.readFile`. */
3256
+ readonly readFile?: (path: string) => Promise<Uint8Array>;
3257
+ /** Per-kind driver table consulted for create / update dispatch. Default-constructed from `ROBLOX_API_KEY` when omitted. */
3258
+ readonly registry?: DriverRegistry;
3259
+ /** Backend used to read the prior snapshot and persist the new one. Default-constructed from `config.state` and `GITHUB_TOKEN` when omitted. */
3260
+ readonly statePort?: StatePort;
3261
+ }
3262
+ /**
3263
+ * Failure surfaced by `deploy`. Stage-tagged so callers can branch on
3264
+ * `kind` to distinguish reconciliation failures (`stateReadFailed`,
3265
+ * `applyFailed`, ...) from default-construction failures
3266
+ * (`configLoadFailed`, `stateNotConfigured`, `unknownEnvironment`,
3267
+ * `incompletePlaceEntry`, `incompleteUniverseEntry`, `missingCredential`,
3268
+ * `unsupportedBackend`, `registryConfigMissing`).
3269
+ */
3270
+ type DeployError = IncompletePlaceEntryError | IncompleteUniverseEntryError | MissingCredentialError | RegistryConfigError | StateNotConfiguredError | UnknownEnvironmentError | UnsupportedBackendError | {
3271
+ readonly cause: ApplyError;
3272
+ readonly kind: "applyFailed";
3273
+ } | {
3274
+ readonly cause: BuildDesiredError;
3275
+ readonly kind: "buildDesiredFailed";
3276
+ } | {
3277
+ readonly cause: ConfigError;
3278
+ readonly kind: "configLoadFailed";
3279
+ } | {
3280
+ readonly cause: StateError;
3281
+ readonly kind: "stateReadFailed";
3282
+ } | {
3283
+ readonly cause: StateError;
3284
+ readonly kind: "stateWriteFailed";
3285
+ readonly unsavedState: BedrockState;
3286
+ };
3287
+ /**
3288
+ * Run a full reconcile end-to-end. Default-constructs missing deps from
3289
+ * the project config and the environment variables `GITHUB_TOKEN` and
3290
+ * `ROBLOX_API_KEY`; never reads `process.env` when `statePort`,
3291
+ * `registry`, and `config` are all supplied explicitly.
3292
+ *
3293
+ * @param options - Target environment plus optional overrides.
3294
+ * @returns The persisted `BedrockState` on success, or a stage-tagged
3295
+ * `DeployError` on failure.
3296
+ * @example
3297
+ *
3298
+ * ```ts
3299
+ * import { deploy } from "@bedrock-rbx/core";
3300
+ *
3301
+ * return deploy({ environment: "production" }).then((result) => {
3302
+ * expect(result.success).toBeFalse();
3303
+ * if (!result.success) {
3304
+ * expect(["configLoadFailed", "stateNotConfigured"]).toContain(result.err.kind);
3305
+ * }
3306
+ * });
3307
+ * ```
3308
+ *
3309
+ * @example
3310
+ *
3311
+ * ```ts
3312
+ * import { deploy, type BedrockState, type DriverRegistry, type StatePort } from "@bedrock-rbx/core";
3313
+ *
3314
+ * const store = new Map<string, BedrockState>();
3315
+ * const statePort: StatePort = {
3316
+ * async read(environment) {
3317
+ * return { data: store.get(environment), success: true };
3318
+ * },
3319
+ * async write(state) {
3320
+ * store.set(state.environment, state);
3321
+ * return { data: undefined, success: true };
3322
+ * },
3323
+ * };
3324
+ * const registry: DriverRegistry = {
3325
+ * developerProduct: {
3326
+ * create: async () => { throw new Error("unreachable: empty config"); },
3327
+ * },
3328
+ * gamePass: { create: async () => { throw new Error("unreachable: empty config"); } },
3329
+ * place: { create: async () => { throw new Error("unreachable: empty config"); } },
3330
+ * universe: { create: async () => { throw new Error("unreachable: empty config"); } },
3331
+ * };
3332
+ *
3333
+ * return deploy({
3334
+ * config: {
3335
+ * environments: { production: {} },
3336
+ * state: { backend: "gist", gistId: "abc" },
3337
+ * passes: {},
3338
+ * },
3339
+ * environment: "production",
3340
+ * registry,
3341
+ * statePort,
3342
+ * }).then((result) => {
3343
+ * expect(result.success).toBeTrue();
3344
+ * if (result.success) {
3345
+ * expect(result.data.environment).toBe("production");
3346
+ * expect(result.data.resources).toBeEmpty();
3347
+ * }
3348
+ * });
3349
+ * ```
3350
+ */
3351
+ declare function deploy(options: DeployOptions): Promise<Result$1<BedrockState, DeployError>>;
3352
+ //#endregion
3353
+ //#region src/shell/migrate-mantle-state.d.ts
3354
+ type ConfigFormat = "typescript" | "yaml";
3355
+ /**
3356
+ * Inputs for `migrateMantleState`. The state file is read via
3357
+ * `readFile` (defaults to `node:fs/promises.readFile`) so callers can
3358
+ * inject in-memory fixtures from tests and the JSDoc `@example` block
3359
+ * stays self-contained.
3360
+ *
3361
+ * `configFormat` selects the output shape: `"typescript"` emits a
3362
+ * `bedrock.config.ts` with `defineConfig({...})`; `"yaml"` emits a
3363
+ * `bedrock.config.yaml` body. Both shapes round-trip through
3364
+ * `loadConfig` cleanly.
3365
+ */
3366
+ interface MigrateMantleStateDeps {
3367
+ /**
3368
+ * Output format for the emitted bedrock config file. `"typescript"`
3369
+ * produces a `defineConfig({...})` module; `"yaml"` produces a YAML
3370
+ * body whose keys match the `Config` schema.
3371
+ */
3372
+ readonly configFormat: ConfigFormat;
3373
+ /**
3374
+ * Environment in the input state file whose resolved values seed
3375
+ * the root config. Required when the state file declares more than
3376
+ * one environment; ignored when only one environment is present.
3377
+ */
3378
+ readonly primaryEnvironment?: string;
3379
+ /**
3380
+ * Reads file bytes; defaults to `node:fs/promises.readFile`. Kept
3381
+ * `Uint8Array`-typed to match `deploy`, `buildDesired`, and
3382
+ * `buildDefaultRegistry`. UTF-8 decoding happens inside the migrator
3383
+ * before YAML parsing.
3384
+ */
3385
+ readonly readFile?: (path: string) => Promise<Uint8Array>;
3386
+ /** Absolute path to the `.mantle-state.yml` file to migrate. */
3387
+ readonly stateFilePath: string;
3388
+ }
3389
+ /**
3390
+ * Read a Mantle state file and produce a `MigrationReport` containing a
3391
+ * bedrock config, per-environment `BedrockState`s, and a structured list
3392
+ * of fields that did not migrate verbatim.
3393
+ *
3394
+ * Skeleton: handles single-environment or multi-environment states with
3395
+ * universe, place, and game-pass resources. The primary environment
3396
+ * auto-picks when only one environment is present; multi-environment
3397
+ * inputs without an explicit `primaryEnvironment` return
3398
+ * `Err({ kind: "primaryEnvironmentRequired", available })` so the
3399
+ * migrator never silently picks a winner. Future slices add social
3400
+ * links and the deferred / blocked warning categories.
3401
+ *
3402
+ * @param deps - Inputs for the migration.
3403
+ * @returns `Ok` with a `MigrationReport` on success, or `Err` with a
3404
+ * discriminated `MigrateError` on failure.
3405
+ * @rejects Re-thrown `readFile` failure when the underlying error code is
3406
+ * not in the recognized "missing file" set; surfaced so callers see
3407
+ * permission or filesystem outages instead of having them coerced to
3408
+ * `stateFileNotFound`.
3409
+ * @example
3410
+ *
3411
+ * ```ts
3412
+ * import { migrateMantleState } from "@bedrock-rbx/core";
3413
+ *
3414
+ * const yaml = [
3415
+ * 'version: "6"',
3416
+ * "environments:",
3417
+ * " production:",
3418
+ * " - id: experience_singleton",
3419
+ * " inputs:",
3420
+ * " experience:",
3421
+ * " groupId: ~",
3422
+ * " outputs:",
3423
+ * " experience:",
3424
+ * " assetId: 6031475575",
3425
+ * " startPlaceId: 17613681043",
3426
+ * " dependencies: []",
3427
+ * "",
3428
+ * ].join("\n");
3429
+ *
3430
+ * async function readFile(): Promise<Uint8Array> {
3431
+ * return new TextEncoder().encode(yaml);
3432
+ * }
3433
+ *
3434
+ * return migrateMantleState({
3435
+ * configFormat: "typescript",
3436
+ * readFile,
3437
+ * stateFilePath: ".mantle-state.yml",
3438
+ * }).then((result) => {
3439
+ * expect(result.success).toBeTrue();
3440
+ * if (result.success) {
3441
+ * expect(result.data.config.universe?.universeId).toBe("6031475575");
3442
+ * }
3443
+ * });
3444
+ * ```
3445
+ */
3446
+ declare function migrateMantleState(deps: MigrateMantleStateDeps): Promise<Result$1<MigrationReport, MigrateError>>;
3447
+ //#endregion
3448
+ export { type ApplyError, type BaseOperation, type BedrockState, type BuildDesiredError, type Config, type ConfigContext, type ConfigEnvironmentUniverseId, type ConfigError, type ConfigInput, type ConfigRootUniverseId, type ConfigValidationIssue, type CreateOperation, DEFAULT_PREFIX_FORMAT, type DeployError, type DeployOptions, type DeveloperProductDesiredInput, type DeveloperProductDesiredState, type DeveloperProductDriverDeps, type DeveloperProductEntry, type DeveloperProductOutputs, type DisplayNamePrefixConfig, type DriverRegistry, type EnvironmentEntry, type GamePassDesiredInput, type GamePassDesiredState, type GamePassDriverDeps, type GamePassEntry, type GamePassOutputs, type GetEnvironmentError, type GistStateAdapterDeps, type GistStateConfig, type IncompletePlaceEntryError, type IncompleteUniverseEntryError, type KindIo, type KindRegistry, type LoadConfigOptions, type MigrateError, type MigrateMantleStateDeps, type MigrationReport, type MigrationSummary, type MigrationWarning, type MissingCredentialError, type NoopOperation, OpenCloudError, type Operation, type PlaceDesiredInput, type PlaceDesiredState, type PlaceDriverDeps, type PlaceEntry, type PlaceOutputs, type PriceFields, type RegistryConfigError, type ResolvedConfig, type ResolvedPlaceEntry, type ResolvedUniverseEntry, type ResourceCurrentState, type ResourceDesiredInput, type ResourceDesiredState, type ResourceDriver, type ResourceEntryByKind, type ResourceKey, type ResourceKind, type ResourceKindModule, type ResourceOutputs, type ResourceOutputsByKind, type Result, type RobloxAssetId, SOCIAL_LINK_FIELDS, type SelectEnvironmentError, type Sha256Hex, type SocialLink, type SocialLinkField, type StateConfig, type StateError, type StateNotConfiguredError, type StatePort, type StatesByEnvironment, UNIVERSE_SINGLETON_KEY, type UniverseDesiredInput, type UniverseDesiredState, type UniverseDriverDeps, type UniverseEntry, type UniverseOutputs, type UniverseOverlayWithId, type UniverseOverlayWithoutId, type UnknownEnvironmentError, type UnsupportedBackendError, type UpdateOperation, applyOps, asResourceKey, asRobloxAssetId, asSha256Hex, buildDefaultRegistry, buildDesired, buildStatePort, createDeveloperProductDriver, createGamePassDriver, createGistStateAdapter, createPlaceDriver, createUniverseDriver, defaultKindRegistry, defineConfig, deploy, derivePriceFields, diff, flattenConfig, getEnvironment, isGistStateConfig, isResourceKey, isRobloxAssetId, isSha256Hex, loadConfig, migrateMantleState, parseStateFile, renderDisplayNamePrefix, resolveStateConfig, selectEnvironment, serializeStateFile, shouldReuploadIcon, validateConfig, validateEnvironmentName, validatePlan };
3449
+ //# sourceMappingURL=index.d.mts.map