@reventlessdev/reventless-aws 3.0.0-alpha.385 → 3.0.0-alpha.387

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.387 (2026-10-19)
7
+
8
+ ### Features
9
+
10
+ * **spec,aws,local:** tell the host shell which environment it runs in, and write local host UI files into a chosen assetsDir ([4f896d1](https://github.com/ReventlessDev/reventless-core/commit/4f896d13ae9ec0d211c33dbd9271e7197cfebc41))
11
+
12
+
13
+ # 3.0.0-alpha.386 (2026-10-17)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/reventless-aws
16
+
17
+
18
+
19
+
20
+
6
21
  # 3.0.0-alpha.385 (2026-10-16)
7
22
 
8
23
  * feat!(inbound): a translation slice can declare a signed webhook URL ([0ad6994](https://github.com/ReventlessDev/reventless-core/commit/0ad69949fcc91c2756c60ca8cb120dd468841d8b))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.385",
3
+ "version": "3.0.0-alpha.387",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -23,18 +23,18 @@
23
23
  "uuid": "^13.0.0",
24
24
  "yaml": "^2.8.3",
25
25
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.32",
26
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.30",
26
27
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
27
28
  "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
28
29
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
29
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.30",
30
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
31
30
  "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
32
31
  "@reventlessdev/rescript-web": "1.0.0-alpha.7",
33
- "@reventlessdev/reventless-infra": "3.0.0-alpha.195",
34
- "@reventlessdev/reventless-core": "3.0.0-alpha.298",
32
+ "@reventlessdev/reventless-core": "3.0.0-alpha.300",
33
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
34
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.197",
35
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.164",
35
36
  "@reventlessdev/reventless-interop": "3.0.0-alpha.41",
36
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.162",
37
- "@reventlessdev/reventless-spec": "3.0.0-alpha.167"
37
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.169"
38
38
  },
39
39
  "devDependencies": {
40
40
  "rescript": "12.3.1",
package/src/Platform.res CHANGED
@@ -2355,6 +2355,12 @@ module MakeWithConfig = (
2355
2355
  // `platformApiEndpoint`, already present) with the store it declares, so there
2356
2356
  // is no per-store URL to publish. The store's *serving* base URL still travels
2357
2357
  // in the `objectStores` stack output above.
2358
+ // Outside the apply; the same name the deployment record carries.
2359
+ let shellEnvironment = Reventless.ShellEnvironment.resolve(
2360
+ ~provenance=Reventless.DeploymentProvenance.current(),
2361
+ ~stack=stackName,
2362
+ ~prodStacks=Util_HostUiDomain.resolveProdStacks(),
2363
+ )
2358
2364
  let configJsonContent =
2359
2365
  (
2360
2366
  (
@@ -2407,6 +2413,7 @@ module MakeWithConfig = (
2407
2413
  // public Function URL and its config key are both gone.
2408
2414
  Util_ShellConfig.fields(
2409
2415
  ~computed=withEvents,
2416
+ ~environment=shellEnvironment,
2410
2417
  ~viewModes=?cfg.viewModes,
2411
2418
  ~bakedManifest=?cfg.bakedManifest,
2412
2419
  ~uiSlotsFile=?cfg.uiSlotsFile,
@@ -35,6 +35,7 @@ import * as Plugin_Stack$ReventlessAws from "./plugin/stack/Plugin_Stack.res.mjs
35
35
  import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
36
36
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
37
37
  import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
38
+ import * as ShellEnvironment$Reventless from "@reventlessdev/reventless-spec/src/types/ShellEnvironment.res.mjs";
38
39
  import * as Platform_Casts$ReventlessAws from "./Platform_Casts.res.mjs";
39
40
  import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
40
41
  import * as QueryDbBackend$ReventlessAws from "./adapter/QueryDb/QueryDbBackend.res.mjs";
@@ -52,6 +53,7 @@ import * as Util_StoreLayout$ReventlessAws from "./util/Util_StoreLayout.res.mjs
52
53
  import * as IndexJs from "@pulumi/pulumi/runtime/index.js";
53
54
  import * as AppSync_EventsApi$ReventlessAws from "./adapter/Api/AppSync_EventsApi.res.mjs";
54
55
  import * as AppSync_MergedApi$ReventlessAws from "./components/Api/AppSync_MergedApi.res.mjs";
56
+ import * as DeploymentProvenance$Reventless from "@reventlessdev/reventless-spec/src/types/DeploymentProvenance.res.mjs";
55
57
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
56
58
  import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/NoEventMappings.res.mjs";
57
59
  import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
@@ -1235,6 +1237,7 @@ function MakeWithConfig(Config) {
1235
1237
  if (capabilities.includes("Messaging") && !emailSenderProvisioned) {
1236
1238
  Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Messaging capability, but this platform provisions no email sender.\n The sender is configuration and has no default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or the env var REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES.\n A stack that should not mail anybody sets \`platform:messagingEmailProvider: log\` instead — every message is logged and none is sent, and the address is optional.\n The root must also pass \`~messagingSender\` in \`~hostUiBundle\`, from \`ReventlessAws.Capability_Messaging.make(~name=…)\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
1237
1239
  }
1240
+ let shellEnvironment = ShellEnvironment$Reventless.resolve(DeploymentProvenance$Reventless.current(), stackName, Util_HostUiDomain$ReventlessAws.resolveProdStacks());
1238
1241
  let configJsonContent = Pulumi.all([
1239
1242
  Pulumi.all([
1240
1243
  resolvedDomainApiEndpoint,
@@ -1286,7 +1289,7 @@ function MakeWithConfig(Config) {
1286
1289
  AppSync_EventsApi$ReventlessAws.clientNamespaceName
1287
1290
  ]
1288
1291
  ]) : computed;
1289
- return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
1292
+ return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, shellEnvironment, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
1290
1293
  });
1291
1294
  let bootFileCacheControl = "no-cache";
1292
1295
  new (Aws.s3.BucketObject)("host-ui-config-json", {
@@ -2628,6 +2631,7 @@ function Make($star) {
2628
2631
  if (capabilities.includes("Messaging") && !emailSenderProvisioned) {
2629
2632
  Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Messaging capability, but this platform provisions no email sender.\n The sender is configuration and has no default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or the env var REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES.\n A stack that should not mail anybody sets \`platform:messagingEmailProvider: log\` instead — every message is logged and none is sent, and the address is optional.\n The root must also pass \`~messagingSender\` in \`~hostUiBundle\`, from \`ReventlessAws.Capability_Messaging.make(~name=…)\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
2630
2633
  }
2634
+ let shellEnvironment = ShellEnvironment$Reventless.resolve(DeploymentProvenance$Reventless.current(), stackName, Util_HostUiDomain$ReventlessAws.resolveProdStacks());
2631
2635
  let configJsonContent = Pulumi.all([
2632
2636
  Pulumi.all([
2633
2637
  resolvedDomainApiEndpoint,
@@ -2679,7 +2683,7 @@ function Make($star) {
2679
2683
  AppSync_EventsApi$ReventlessAws.clientNamespaceName
2680
2684
  ]
2681
2685
  ]) : computed;
2682
- return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
2686
+ return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, shellEnvironment, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
2683
2687
  });
2684
2688
  let bootFileCacheControl = "no-cache";
2685
2689
  new (Aws.s3.BucketObject)("host-ui-config-json", {
@@ -81,7 +81,8 @@ let subscriptionEndpoint = (httpsEndpoint: string): string =>
81
81
  /**
82
82
  The config.json field set.
83
83
 
84
- `computed` arrives in wire order and keeps it. `viewModes` unset ⇒ no
84
+ `computed` arrives in wire order and keeps it; `environment` follows it, its
85
+ purpose key only when the purpose is known. `viewModes` unset ⇒ no
85
86
  `viewModes` key and no per-mode key, so a deployment that wants no optional mode
86
87
  gets byte-identical output to before this input existed. `bakedManifest` unset ⇒
87
88
  no `manifestUrl`, and every caller keeps the admin discovery path. `shellConfig`
@@ -91,6 +92,7 @@ somewhere unintended with nothing in the diff to say so.
91
92
  */
92
93
  let fields = (
93
94
  ~computed: array<(string, JSON.t)>,
95
+ ~environment: option<Reventless.ShellEnvironment.t>=?,
94
96
  ~viewModes: option<array<Platform.viewMode>>=?,
95
97
  ~bakedManifest: option<Platform.bakedManifest>=?,
96
98
  ~uiSlotsFile: option<string>=?,
@@ -98,6 +100,10 @@ let fields = (
98
100
  ): dict<JSON.t> => {
99
101
  let out = computed->Dict.fromArray
100
102
 
103
+ environment->Option.forEach(env =>
104
+ env->Reventless.ShellEnvironment.configFields->Array.forEach(((k, v)) => out->Dict.set(k, v))
105
+ )
106
+
101
107
  // Computed, like the endpoints: the group the admin API is gated on, from the
102
108
  // deploy's own role mapping.
103
109
  ReventlessCore.Platform_AdminGroup.configEntry()->Option.forEach(((k, v)) => out->Dict.set(k, v))
@@ -4,6 +4,7 @@ import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.js";
4
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Platform$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Platform.res.mjs";
7
+ import * as ShellEnvironment$Reventless from "@reventlessdev/reventless-spec/src/types/ShellEnvironment.res.mjs";
7
8
  import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
8
9
  import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
9
10
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
@@ -58,8 +59,13 @@ function subscriptionEndpoint(httpsEndpoint) {
58
59
  return httpsEndpoint.replace("https://", "wss://").replace(".appsync-api.", ".appsync-realtime-api.");
59
60
  }
60
61
 
61
- function fields(computed, viewModes, bakedManifest, uiSlotsFile, shellConfig) {
62
+ function fields(computed, environment, viewModes, bakedManifest, uiSlotsFile, shellConfig) {
62
63
  let out = Object.fromEntries(computed);
64
+ Stdlib_Option.forEach(environment, env => {
65
+ ShellEnvironment$Reventless.configFields(env).forEach(param => {
66
+ out[param[0]] = param[1];
67
+ });
68
+ });
63
69
  Stdlib_Option.forEach(Platform_AdminGroup$ReventlessCore.configEntry(), param => {
64
70
  out[param[0]] = param[1];
65
71
  });
@@ -107,4 +113,4 @@ export {
107
113
  subscriptionEndpoint,
108
114
  fields,
109
115
  }
110
- /* Platform_AdminGroup-ReventlessCore Not a pure module */
116
+ /* ShellEnvironment-Reventless Not a pure module */
@@ -226,6 +226,46 @@ describe("Util_ShellConfig.fields — journeys", () => {
226
226
  })
227
227
  })
228
228
 
229
+ // The shell badges every environment that is not production, so a purpose that
230
+ // went missing shows as a badge rather than as nothing.
231
+ describe("Util_ShellConfig.fields — environment", () => {
232
+ testSync("the name and the purpose follow the computed keys", () => {
233
+ let out = Util_ShellConfig.fields(~computed, ~environment={name: "test", purpose: Some("test")})
234
+ expect(out->Dict.keysToArray)->toEqual([
235
+ "apiEndpoint",
236
+ "region",
237
+ "environment",
238
+ "environmentPurpose",
239
+ ])
240
+ expect((out->get("environment"), out->get("environmentPurpose")))->toEqual((
241
+ JSON.Encode.string("test"),
242
+ JSON.Encode.string("test"),
243
+ ))
244
+ })
245
+
246
+ testSync("an unknown purpose leaves its key out", () => {
247
+ let out = Util_ShellConfig.fields(~computed, ~environment={name: "alpha", purpose: None})
248
+ expect(out->Dict.get("environmentPurpose")->Option.isNone)->toBe(true)
249
+ })
250
+
251
+ testSync("a shellConfig environment fails the deploy rather than renaming it", () => {
252
+ let failure = try {
253
+ let _ = Util_ShellConfig.fields(
254
+ ~computed,
255
+ ~environment={name: "prod", purpose: Some("production")},
256
+ ~shellConfig=Dict.fromArray([("environment", JSON.Encode.string("staging"))]),
257
+ )
258
+ None
259
+ } catch {
260
+ | Failure(message) => Some(message)
261
+ }
262
+ switch failure {
263
+ | Some(message) => expect(message->String.includes("environment"))->toBe(true)
264
+ | None => fail("a shellConfig key renaming the computed environment must fail the deploy")
265
+ }
266
+ })
267
+ })
268
+
229
269
  describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
230
270
  testSync("shell-owned keys land verbatim, under the computed ones", () => {
231
271
  let out = Util_ShellConfig.fields(
@@ -28,7 +28,7 @@ function get(out, key) {
28
28
 
29
29
  globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
30
30
  globalThis.test("unset ⇒ no viewModes key and the computed set is untouched", () => {
31
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined);
31
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, undefined);
32
32
  globalThis.expect(Object.keys(out)).toEqual([
33
33
  "apiEndpoint",
34
34
  "region"
@@ -36,7 +36,7 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
36
36
  globalThis.expect(Stdlib_Option.isNone(out["viewModes"])).toBe(true);
37
37
  });
38
38
  globalThis.test("Map with defaults ⇒ the mode, and no mapStyle key", () => {
39
- let out = Util_ShellConfig$ReventlessAws.fields(computed, [{
39
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, [{
40
40
  TAG: "Map",
41
41
  _0: {}
42
42
  }], undefined, undefined, undefined);
@@ -44,7 +44,7 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
44
44
  globalThis.expect(Stdlib_Option.isNone(out["mapStyle"])).toBe(true);
45
45
  });
46
46
  globalThis.test("per-mode options flatten beside the mode", () => {
47
- let out = Util_ShellConfig$ReventlessAws.fields(computed, [
47
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, [
48
48
  {
49
49
  TAG: "Map",
50
50
  _0: {
@@ -77,21 +77,21 @@ globalThis.describe("Util_ShellConfig.fields — bakedManifest", () => {
77
77
  key: key
78
78
  });
79
79
  globalThis.test("unset ⇒ no manifestUrl, so every caller keeps the admin path", () => {
80
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined);
80
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, undefined);
81
81
  globalThis.expect(Stdlib_Option.isNone(out["manifestUrl"])).toBe(true);
82
82
  });
83
83
  globalThis.test("declared ⇒ the URL the deploy writes the file to", () => {
84
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake(undefined), undefined, undefined);
84
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, bake(undefined), undefined, undefined);
85
85
  globalThis.expect(get(out, "manifestUrl")).toEqual("/component-manifest.json");
86
86
  });
87
87
  globalThis.test("follows a renamed bake", () => {
88
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake("storefront.json"), undefined, undefined);
88
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, bake("storefront.json"), undefined, undefined);
89
89
  globalThis.expect(get(out, "manifestUrl")).toEqual("/storefront.json");
90
90
  });
91
91
  globalThis.test("a shellConfig manifestUrl fails the deploy rather than redirecting it", () => {
92
92
  let failure;
93
93
  try {
94
- Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake(undefined), undefined, Object.fromEntries([[
94
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, bake(undefined), undefined, Object.fromEntries([[
95
95
  "manifestUrl",
96
96
  "/elsewhere.json"
97
97
  ]]));
@@ -115,21 +115,21 @@ globalThis.describe("Util_ShellConfig.fields — bakedManifest", () => {
115
115
 
116
116
  globalThis.describe("Util_ShellConfig.fields — uiSlotsFile", () => {
117
117
  globalThis.test("unset ⇒ no uiSlotsUrl, and the shell imports nothing", () => {
118
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined);
118
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, undefined);
119
119
  globalThis.expect(Stdlib_Option.isNone(out["uiSlotsUrl"])).toBe(true);
120
120
  });
121
121
  globalThis.test("declared ⇒ the URL the deploy writes the module to", () => {
122
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, "/src/storefront-slots.js", undefined);
122
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, "/src/storefront-slots.js", undefined);
123
123
  globalThis.expect(get(out, "uiSlotsUrl")).toEqual("/ui-slots.js");
124
124
  });
125
125
  globalThis.test("names where the file is served, never where it was authored", () => {
126
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, "/home/me/prj/slots.js", undefined);
126
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, "/home/me/prj/slots.js", undefined);
127
127
  globalThis.expect(get(out, "uiSlotsUrl")).toEqual("/ui-slots.js");
128
128
  });
129
129
  globalThis.test("a shellConfig uiSlotsUrl fails the deploy rather than redirecting it", () => {
130
130
  let failure;
131
131
  try {
132
- Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, "/src/storefront-slots.js", Object.fromEntries([[
132
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, "/src/storefront-slots.js", Object.fromEntries([[
133
133
  "uiSlotsUrl",
134
134
  "/elsewhere.js"
135
135
  ]]));
@@ -181,7 +181,7 @@ globalThis.describe("Util_ShellConfig.fields — journeys", () => {
181
181
  key: fulfilment_key
182
182
  };
183
183
  globalThis.test("a bake declaring no journeys writes no map", () => {
184
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, {
184
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, {
185
185
  components: [{
186
186
  plugin: "Catalog",
187
187
  views: ["Products"],
@@ -191,15 +191,15 @@ globalThis.describe("Util_ShellConfig.fields — journeys", () => {
191
191
  globalThis.expect(Stdlib_Option.isNone(out["journeyManifestUrls"])).toBe(true);
192
192
  });
193
193
  globalThis.test("an empty journeys array is the same as none", () => {
194
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, withJourneys([]), undefined, undefined);
194
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, withJourneys([]), undefined, undefined);
195
195
  globalThis.expect(Stdlib_Option.isNone(out["journeyManifestUrls"])).toBe(true);
196
196
  });
197
197
  globalThis.test("keeps manifestUrl as the default journey", () => {
198
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, withJourneys([shopper]), undefined, undefined);
198
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, withJourneys([shopper]), undefined, undefined);
199
199
  globalThis.expect(get(out, "manifestUrl")).toEqual("/component-manifest.json");
200
200
  });
201
201
  globalThis.test("maps each declared group to its own file", () => {
202
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, withJourneys([
202
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, withJourneys([
203
203
  shopper,
204
204
  fulfilment
205
205
  ]), undefined, undefined);
@@ -219,14 +219,14 @@ globalThis.describe("Util_ShellConfig.fields — journeys", () => {
219
219
  shopper,
220
220
  fulfilment
221
221
  ]);
222
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake, undefined, undefined);
222
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, bake, undefined, undefined);
223
223
  let published = Stdlib_Option.mapOr(Stdlib_JSON.Decode.string(get(out, "manifestUrl")), [], url => [url]).concat(Stdlib_Array.filterMap(Object.values(Stdlib_Option.getOr(Stdlib_JSON.Decode.object(get(out, "journeyManifestUrls")), {})), Stdlib_JSON.Decode.string));
224
224
  globalThis.expect(published).toEqual(Platform_BakedManifest$ReventlessCore.files(bake).map(param => "/" + param[0]));
225
225
  });
226
226
  globalThis.test("a shellConfig journey map fails the deploy rather than redirecting it", () => {
227
227
  let failure;
228
228
  try {
229
- Util_ShellConfig$ReventlessAws.fields(computed, undefined, withJourneys([shopper]), undefined, Object.fromEntries([[
229
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, withJourneys([shopper]), undefined, Object.fromEntries([[
230
230
  "journeyManifestUrls",
231
231
  {}
232
232
  ]]));
@@ -248,9 +248,64 @@ globalThis.describe("Util_ShellConfig.fields — journeys", () => {
248
248
  });
249
249
  });
250
250
 
251
+ globalThis.describe("Util_ShellConfig.fields — environment", () => {
252
+ globalThis.test("the name and the purpose follow the computed keys", () => {
253
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, {
254
+ name: "test",
255
+ purpose: "test"
256
+ }, undefined, undefined, undefined, undefined);
257
+ globalThis.expect(Object.keys(out)).toEqual([
258
+ "apiEndpoint",
259
+ "region",
260
+ "environment",
261
+ "environmentPurpose"
262
+ ]);
263
+ globalThis.expect([
264
+ get(out, "environment"),
265
+ get(out, "environmentPurpose")
266
+ ]).toEqual([
267
+ "test",
268
+ "test"
269
+ ]);
270
+ });
271
+ globalThis.test("an unknown purpose leaves its key out", () => {
272
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, {
273
+ name: "alpha",
274
+ purpose: undefined
275
+ }, undefined, undefined, undefined, undefined);
276
+ globalThis.expect(Stdlib_Option.isNone(out["environmentPurpose"])).toBe(true);
277
+ });
278
+ globalThis.test("a shellConfig environment fails the deploy rather than renaming it", () => {
279
+ let failure;
280
+ try {
281
+ Util_ShellConfig$ReventlessAws.fields(computed, {
282
+ name: "prod",
283
+ purpose: "production"
284
+ }, undefined, undefined, undefined, Object.fromEntries([[
285
+ "environment",
286
+ "staging"
287
+ ]]));
288
+ failure = undefined;
289
+ } catch (raw_message) {
290
+ let message = Primitive_exceptions.internalToException(raw_message);
291
+ if (message.RE_EXN_ID === "Failure") {
292
+ failure = message._1;
293
+ } else {
294
+ throw message;
295
+ }
296
+ }
297
+ if (failure !== undefined) {
298
+ globalThis.expect(failure.includes("environment")).toBe(true);
299
+ return;
300
+ } else {
301
+ return JestGlobals.fail("a shellConfig key renaming the computed environment must fail the deploy");
302
+ }
303
+ });
304
+ });
305
+
251
306
  globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
252
307
  globalThis.test("shell-owned keys land verbatim, under the computed ones", () => {
253
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, Object.fromEntries([
308
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, Object.fromEntries([
254
309
  [
255
310
  "platformName",
256
311
  "Online Shop"
@@ -271,7 +326,7 @@ globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () =>
271
326
  globalThis.test("a key the deploy computes fails the deploy, naming it", () => {
272
327
  let failure;
273
328
  try {
274
- Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, Object.fromEntries([[
329
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, Object.fromEntries([[
275
330
  "apiEndpoint",
276
331
  "https://wrong.example/graphql"
277
332
  ]]));
@@ -345,7 +400,7 @@ globalThis.describe("the subscription endpoint", () => {
345
400
  globalThis.describe("Util_ShellConfig.fields — adminGroup", () => {
346
401
  globalThis.test("is absent while the administrator role keeps its own name", () => {
347
402
  Role$Reventless.clearGroups();
348
- globalThis.expect(Stdlib_Option.isNone(Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined)["adminGroup"])).toBe(true);
403
+ globalThis.expect(Stdlib_Option.isNone(Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, undefined)["adminGroup"])).toBe(true);
349
404
  });
350
405
  globalThis.test("carries the group a renamed administrator role maps to", () => {
351
406
  Role$Reventless.clearGroups();
@@ -353,7 +408,7 @@ globalThis.describe("Util_ShellConfig.fields — adminGroup", () => {
353
408
  Role$Reventless.admin,
354
409
  "shop-admins"
355
410
  ]]);
356
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined);
411
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined, undefined);
357
412
  Role$Reventless.clearGroups();
358
413
  globalThis.expect(get(out, "adminGroup")).toEqual("shop-admins");
359
414
  });