@reventlessdev/reventless-aws 3.0.0-alpha.333 → 3.0.0-alpha.334

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,20 @@
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.334 (2026-09-02)
7
+
8
+ * feat(aws)!: the messaging sender is configuration, and a stack can choose to only log ([23b8b4b](https://github.com/ReventlessDev/reventless-core/commit/23b8b4bfe9c70555de4d74266ca686cb427485ca))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * `Capability_Messaging_Ses.make` is replaced by
13
+ `Capability_Messaging.make(~name)`, which reads the transport and the address
14
+ from config; the SES module keeps only `emailSender`. A deployment that named its
15
+ sender in code must move it to `platform:messagingEmailSender` or the deploy is
16
+ refused.
17
+
18
+
19
+
6
20
  # 3.0.0-alpha.333 (2026-09-02)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.333",
3
+ "version": "3.0.0-alpha.334",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -15,18 +15,18 @@
15
15
  "@aws-sdk/s3-request-presigner": "3.970.0",
16
16
  "sury": "11.0.0-rc.2",
17
17
  "uuid": "^13.0.0",
18
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.14",
19
18
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
20
19
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
21
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.7",
20
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.14",
22
21
  "@reventlessdev/rescript-node": "2.0.0-alpha.8",
22
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.7",
23
23
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
24
+ "@reventlessdev/reventless-core": "3.0.0-alpha.254",
25
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.155",
24
26
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
25
- "@reventlessdev/reventless-core": "3.0.0-alpha.253",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.154",
27
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.117",
28
27
  "@reventlessdev/reventless-interop": "3.0.0-alpha.34",
29
- "@reventlessdev/reventless-spec": "3.0.0-alpha.126"
28
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.118",
29
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.127"
30
30
  },
31
31
  "devDependencies": {
32
32
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -103,6 +103,10 @@ let geocoderPlaceIndexRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make
103
103
  same `""` sentinel and the same reason. */
104
104
  let messagingEmailSenderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
105
105
 
106
+ /** The email transport behind that address, carried the same way for the same case.
107
+ `""` means the platform named none, which the runtime reads as its default. */
108
+ let messagingEmailProviderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
109
+
106
110
  module MakeWithConfig = (
107
111
  Config: {
108
112
  let splitApi: bool
@@ -2178,16 +2182,57 @@ module MakeWithConfig = (
2178
2182
  Pulumi.Pulumi.export("messagingEmailSender", messagingEmailSenderFlat)
2179
2183
  messagingEmailSenderRef := messagingEmailSenderFlat
2180
2184
 
2185
+ // Which transport is behind that sender. Exported and threaded exactly like
2186
+ // the address, because a plugin stack's Lambdas need both and read them the
2187
+ // same way — the address alone would leave a runtime guessing what to send
2188
+ // it with. `""` means "the platform did not say", which the runtime reads as
2189
+ // the default rather than as an error.
2190
+ let messagingEmailProviderFlat = switch cfg.messagingSender {
2191
+ | Some({emailProvider: ?Some(name)}) => name->Pulumi.Output.fromInput
2192
+ | _ => Pulumi.Output.make("")
2193
+ }
2194
+ Pulumi.Pulumi.export("messagingEmailProvider", messagingEmailProviderFlat)
2195
+ messagingEmailProviderRef := messagingEmailProviderFlat
2196
+
2197
+ // Exported but not handed to a Lambda: nothing reads an SMS sender yet, and
2198
+ // an env var no runtime consults is a setting that looks wired. The export
2199
+ // is how a stack can see what it configured.
2200
+ Pulumi.Pulumi.export(
2201
+ "messagingSmsSender",
2202
+ switch cfg.messagingSender {
2203
+ | Some({smsSender: ?Some(sender)}) => sender->Pulumi.Output.fromInput
2204
+ | _ => Pulumi.Output.make("")
2205
+ },
2206
+ )
2207
+
2181
2208
  // Same gate as geocoding's, and the failure it prevents is worse: an
2182
2209
  // unprovisioned mailer answers `Unavailable` on every send, so the messages
2183
2210
  // queue and retry and are eventually abandoned — a notification nobody
2184
2211
  // receives and no error names.
2185
- if capabilities->Array.includes(Messaging) && cfg.messagingSender->Option.isNone {
2212
+ //
2213
+ // The test is the *sender*, not the handle. Since the address became
2214
+ // configuration with no default, a root that calls the helper hands over a
2215
+ // record either way — one carrying no sender when the stack named none — so
2216
+ // a gate reading `Option.isNone` would pass a deployment with no mailer.
2217
+ // Whether the field is present is known here, without resolving the Output
2218
+ // behind it, which is what keeps this refusal at graph-construction time
2219
+ // rather than in the `apply` further down: the same gap caught there fails
2220
+ // the deploy midway, and is skipped entirely by `pulumi preview`.
2221
+ let emailSenderProvisioned = switch cfg.messagingSender {
2222
+ | Some({emailSender: ?Some(_)}) => true
2223
+ | _ => false
2224
+ }
2225
+ if capabilities->Array.includes(Messaging) && !emailSenderProvisioned {
2186
2226
  JsError.throwWithMessage(
2187
2227
  `A plugin of this deployment declares the Messaging capability, but this platform ` ++
2188
- `provisions no sender.\n` ++
2189
- ` Add \`let sender = ReventlessAws.Capability_Messaging_Ses.make(~name=…, ~email=…)\` ` ++
2190
- `and pass \`~messagingSender=sender\` in \`~hostUiBundle\`.\n` ++
2228
+ `provisions no email sender.\n` ++
2229
+ ` The sender is configuration and has no default: set ` ++
2230
+ `\`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or the env var ` ++
2231
+ `REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES.\n` ++
2232
+ ` A stack that should not mail anybody sets \`platform:messagingEmailProvider: log\` ` ++
2233
+ `instead — every message is logged and none is sent, and the address is optional.\n` ++
2234
+ ` The root must also pass \`~messagingSender\` in \`~hostUiBundle\`, from ` ++
2235
+ `\`ReventlessAws.Capability_Messaging.make(~name=…)\`.\n` ++
2191
2236
  ` The declaration is generated from the plugins' capabilities.json — regenerate with ` ++
2192
2237
  `\`pnpm run generate:platform\` if it is stale.`,
2193
2238
  )
@@ -2516,6 +2561,23 @@ module MakeWithConfig = (
2516
2561
  PluginRuntime_Builder.registerCapabilityEnv("MESSAGING_EMAIL_SENDER", messagingEmailSender)
2517
2562
  PluginRuntime_Builder.registerMessagingSender(messagingEmailSender)
2518
2563
 
2564
+ // The transport, read across the same two halves. Only the env var: the IAM
2565
+ // grant below keys off the address, and a log-transport deployment wants the same
2566
+ // grant anyway — a stack that switches back to SES should not need its roles
2567
+ // rebuilt.
2568
+ let messagingEmailProvider: Pulumi.Output.t<string> = switch platformStackRef {
2569
+ | Some(stackRef) =>
2570
+ (
2571
+ stackRef->Pulumi.StackReference.getOutput("messagingEmailProvider"):
2572
+ Pulumi.Output.t<option<string>>
2573
+ )->Pulumi.Output.apply(o => o->Option.getOr(""))
2574
+ | None => messagingEmailProviderRef.contents
2575
+ }
2576
+ PluginRuntime_Builder.registerCapabilityEnv(
2577
+ "MESSAGING_EMAIL_PROVIDER",
2578
+ messagingEmailProvider,
2579
+ )
2580
+
2519
2581
  module P = unpack(plugin)
2520
2582
  let pluginComponent = P.make()
2521
2583
  ReventlessCore.Plugin_Helpers.clearOffload()
@@ -2698,8 +2760,11 @@ module MakeWithConfig = (
2698
2760
  `\n Geocoding is \`Capability_Geocoding_AwsLocation.make\`, passed to the ` ++
2699
2761
  `platform as \`~geocoderPlaceIndex\`.`
2700
2762
  | Messaging =>
2701
- `\n Messaging is \`Capability_Messaging_Ses.make\`, passed to the platform ` ++
2702
- `as \`~messagingSender\`.`
2763
+ `\n Messaging is \`Capability_Messaging.make\`, passed to the platform ` ++
2764
+ `as \`~messagingSender\`. The sender itself is configuration and has no ` ++
2765
+ `default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or ` ++
2766
+ `REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES — or ` ++
2767
+ `\`platform:messagingEmailProvider: log\` to log every message and send none.`
2703
2768
  }
2704
2769
  )
2705
2770
  // One line per capability, not per declaring component: two slices
@@ -144,6 +144,10 @@ let messagingEmailSenderRef = {
144
144
  contents: Pulumi.output("")
145
145
  };
146
146
 
147
+ let messagingEmailProviderRef = {
148
+ contents: Pulumi.output("")
149
+ };
150
+
147
151
  function MakeWithConfig(Config) {
148
152
  Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
149
153
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
@@ -1168,8 +1172,29 @@ function MakeWithConfig(Config) {
1168
1172
  }
1169
1173
  Pulumi$Pulumi.$$export("messagingEmailSender", messagingEmailSenderFlat);
1170
1174
  messagingEmailSenderRef.contents = messagingEmailSenderFlat;
1171
- if (capabilities.includes("Messaging") && Stdlib_Option.isNone(hostUiBundle.messagingSender)) {
1172
- Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Messaging capability, but this platform provisions no sender.\n Add \`let sender = ReventlessAws.Capability_Messaging_Ses.make(~name=…, ~email=…)\` and pass \`~messagingSender=sender\` in \`~hostUiBundle\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
1175
+ let match$6 = hostUiBundle.messagingSender;
1176
+ let messagingEmailProviderFlat;
1177
+ if (match$6 !== undefined) {
1178
+ let name = match$6.emailProvider;
1179
+ messagingEmailProviderFlat = name !== undefined ? name : Pulumi.output("");
1180
+ } else {
1181
+ messagingEmailProviderFlat = Pulumi.output("");
1182
+ }
1183
+ Pulumi$Pulumi.$$export("messagingEmailProvider", messagingEmailProviderFlat);
1184
+ messagingEmailProviderRef.contents = messagingEmailProviderFlat;
1185
+ let match$7 = hostUiBundle.messagingSender;
1186
+ let tmp$2;
1187
+ if (match$7 !== undefined) {
1188
+ let sender$1 = match$7.smsSender;
1189
+ tmp$2 = sender$1 !== undefined ? sender$1 : Pulumi.output("");
1190
+ } else {
1191
+ tmp$2 = Pulumi.output("");
1192
+ }
1193
+ Pulumi$Pulumi.$$export("messagingSmsSender", tmp$2);
1194
+ let match$8 = hostUiBundle.messagingSender;
1195
+ let emailSenderProvisioned = match$8 !== undefined ? match$8.emailSender !== undefined : false;
1196
+ if (capabilities.includes("Messaging") && !emailSenderProvisioned) {
1197
+ 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.`);
1173
1198
  }
1174
1199
  let configJsonContent = Pulumi.all([
1175
1200
  Pulumi.all([
@@ -1240,9 +1265,9 @@ function MakeWithConfig(Config) {
1240
1265
  contentType: "application/json"
1241
1266
  });
1242
1267
  }
1243
- let match$6 = hostUiBundle.bakedManifest;
1244
- if (componentDefinitions !== undefined && match$6 !== undefined) {
1245
- let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$6).map(param => param[0]);
1268
+ let match$9 = hostUiBundle.bakedManifest;
1269
+ if (componentDefinitions !== undefined && match$9 !== undefined) {
1270
+ let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$9).map(param => param[0]);
1246
1271
  let manifestKey = Stdlib_Option.getOr(manifestKeys[0], Platform_BakedManifest$ReventlessCore.defaultKey);
1247
1272
  Pulumi.all([
1248
1273
  componentDefinitions.roleId,
@@ -1326,6 +1351,8 @@ function MakeWithConfig(Config) {
1326
1351
  let messagingEmailSender = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailSender").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailSenderRef.contents;
1327
1352
  PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_SENDER", messagingEmailSender);
1328
1353
  PluginRuntime_Builder$ReventlessAws.registerMessagingSender(messagingEmailSender);
1354
+ let messagingEmailProvider = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailProvider").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailProviderRef.contents;
1355
+ PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_PROVIDER", messagingEmailProvider);
1329
1356
  let pluginComponent = plugin.make();
1330
1357
  Plugin_Helpers$ReventlessCore.clearOffload();
1331
1358
  currentDeployTarget.contents = "Domain";
@@ -1398,7 +1425,7 @@ function MakeWithConfig(Config) {
1398
1425
  if (match === "Geocoding") {
1399
1426
  return `\n Geocoding is \`Capability_Geocoding_AwsLocation.make\`, passed to the platform as \`~geocoderPlaceIndex\`.`;
1400
1427
  } else {
1401
- return `\n Messaging is \`Capability_Messaging_Ses.make\`, passed to the platform as \`~messagingSender\`.`;
1428
+ return `\n Messaging is \`Capability_Messaging.make\`, passed to the platform as \`~messagingSender\`. The sender itself is configuration and has no default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES — or \`platform:messagingEmailProvider: log\` to log every message and send none.`;
1402
1429
  }
1403
1430
  }))).join(""));
1404
1431
  }
@@ -2506,8 +2533,29 @@ function Make($star) {
2506
2533
  }
2507
2534
  Pulumi$Pulumi.$$export("messagingEmailSender", messagingEmailSenderFlat);
2508
2535
  messagingEmailSenderRef.contents = messagingEmailSenderFlat;
2509
- if (capabilities.includes("Messaging") && Stdlib_Option.isNone(hostUiBundle.messagingSender)) {
2510
- Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Messaging capability, but this platform provisions no sender.\n Add \`let sender = ReventlessAws.Capability_Messaging_Ses.make(~name=…, ~email=…)\` and pass \`~messagingSender=sender\` in \`~hostUiBundle\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
2536
+ let match$6 = hostUiBundle.messagingSender;
2537
+ let messagingEmailProviderFlat;
2538
+ if (match$6 !== undefined) {
2539
+ let name = match$6.emailProvider;
2540
+ messagingEmailProviderFlat = name !== undefined ? name : Pulumi.output("");
2541
+ } else {
2542
+ messagingEmailProviderFlat = Pulumi.output("");
2543
+ }
2544
+ Pulumi$Pulumi.$$export("messagingEmailProvider", messagingEmailProviderFlat);
2545
+ messagingEmailProviderRef.contents = messagingEmailProviderFlat;
2546
+ let match$7 = hostUiBundle.messagingSender;
2547
+ let tmp$2;
2548
+ if (match$7 !== undefined) {
2549
+ let sender$1 = match$7.smsSender;
2550
+ tmp$2 = sender$1 !== undefined ? sender$1 : Pulumi.output("");
2551
+ } else {
2552
+ tmp$2 = Pulumi.output("");
2553
+ }
2554
+ Pulumi$Pulumi.$$export("messagingSmsSender", tmp$2);
2555
+ let match$8 = hostUiBundle.messagingSender;
2556
+ let emailSenderProvisioned = match$8 !== undefined ? match$8.emailSender !== undefined : false;
2557
+ if (capabilities.includes("Messaging") && !emailSenderProvisioned) {
2558
+ 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.`);
2511
2559
  }
2512
2560
  let configJsonContent = Pulumi.all([
2513
2561
  Pulumi.all([
@@ -2578,9 +2626,9 @@ function Make($star) {
2578
2626
  contentType: "application/json"
2579
2627
  });
2580
2628
  }
2581
- let match$6 = hostUiBundle.bakedManifest;
2582
- if (componentDefinitions !== undefined && match$6 !== undefined) {
2583
- let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$6).map(param => param[0]);
2629
+ let match$9 = hostUiBundle.bakedManifest;
2630
+ if (componentDefinitions !== undefined && match$9 !== undefined) {
2631
+ let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$9).map(param => param[0]);
2584
2632
  let manifestKey = Stdlib_Option.getOr(manifestKeys[0], Platform_BakedManifest$ReventlessCore.defaultKey);
2585
2633
  Pulumi.all([
2586
2634
  componentDefinitions.roleId,
@@ -2662,6 +2710,8 @@ function Make($star) {
2662
2710
  let messagingEmailSender = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailSender").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailSenderRef.contents;
2663
2711
  PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_SENDER", messagingEmailSender);
2664
2712
  PluginRuntime_Builder$ReventlessAws.registerMessagingSender(messagingEmailSender);
2713
+ let messagingEmailProvider = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailProvider").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailProviderRef.contents;
2714
+ PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_PROVIDER", messagingEmailProvider);
2665
2715
  let pluginComponent = plugin.make();
2666
2716
  Plugin_Helpers$ReventlessCore.clearOffload();
2667
2717
  currentDeployTarget.contents = "Domain";
@@ -2734,7 +2784,7 @@ function Make($star) {
2734
2784
  if (match === "Geocoding") {
2735
2785
  return `\n Geocoding is \`Capability_Geocoding_AwsLocation.make\`, passed to the platform as \`~geocoderPlaceIndex\`.`;
2736
2786
  } else {
2737
- return `\n Messaging is \`Capability_Messaging_Ses.make\`, passed to the platform as \`~messagingSender\`.`;
2787
+ return `\n Messaging is \`Capability_Messaging.make\`, passed to the platform as \`~messagingSender\`. The sender itself is configuration and has no default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES — or \`platform:messagingEmailProvider: log\` to log every message and send none.`;
2738
2788
  }
2739
2789
  }))).join(""));
2740
2790
  }
@@ -2855,6 +2905,7 @@ export {
2855
2905
  getObjectStoreEndpoints,
2856
2906
  geocoderPlaceIndexRef,
2857
2907
  messagingEmailSenderRef,
2908
+ messagingEmailProviderRef,
2858
2909
  MakeWithConfig,
2859
2910
  Make,
2860
2911
  }
@@ -297,15 +297,30 @@ outcome, not a verdict on the address. `MESSAGING_EMAIL_SENDER` is read the same
297
297
  way, and empty yields a provider with no channels rather than one that claims a
298
298
  channel it cannot send on.
299
299
  */
300
+ /** The email transport this deployment chose.
301
+
302
+ Read per call like the sender beside it, and defaulting to SES on anything it
303
+ does not recognise — including the empty string a platform that named nothing
304
+ exports. The deploy already refuses an unknown value (`Capability_Messaging`
305
+ parses it there, where a human is watching); defaulting again here is for the
306
+ case that cannot be refused, a Lambda whose variable was edited by hand. SES
307
+ is the safe direction: a message that should have been logged and was sent is
308
+ recoverable, a customer notification silently swallowed by a log line is not. */
309
+ let messagingEmailProvider = (): Reventless.Messaging.provider => {
310
+ let sender = NodeProcess.env->Dict.get("MESSAGING_EMAIL_SENDER")->Option.getOr("")
311
+ switch NodeProcess.env->Dict.get("MESSAGING_EMAIL_PROVIDER")->Option.getOr("") {
312
+ | "log" => ReventlessCore.Messaging_Log_Backend.provider(~sender)
313
+ | _ => Messaging_Ses_Backend.provider(~sender)
314
+ }
315
+ }
316
+
300
317
  let capabilities = (): Reventless.Capabilities.t => {
301
318
  geocode: (~text) =>
302
319
  Geocoder_AwsLocation_Backend.search(
303
320
  ~indexName=NodeProcess.env->Dict.get("PLACE_INDEX_NAME")->Option.getOr(""),
304
321
  ~text,
305
322
  ),
306
- messaging: Messaging_Ses_Backend.provider(
307
- ~sender=NodeProcess.env->Dict.get("MESSAGING_EMAIL_SENDER")->Option.getOr(""),
308
- ),
323
+ messaging: messagingEmailProvider(),
309
324
  }
310
325
 
311
326
  // ── Phase-1/phase-2 pipelines ───────────────────────────────────────────────
@@ -16,6 +16,7 @@ import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src
16
16
  import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
17
17
  import * as Messaging_Ses_Backend$ReventlessAws from "../Messaging/Messaging_Ses_Backend.res.mjs";
18
18
  import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
19
+ import * as Messaging_Log_Backend$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Messaging/Messaging_Log_Backend.res.mjs";
19
20
  import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
20
21
  import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
21
22
  import * as Geocoder_AwsLocation_Backend$ReventlessAws from "../Geocoder/Geocoder_AwsLocation_Backend.res.mjs";
@@ -185,10 +186,20 @@ function makeLoadTodoItems(queryDbTableName, todoItems, rowSchema, comp) {
185
186
  };
186
187
  }
187
188
 
189
+ function messagingEmailProvider() {
190
+ let sender = Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_SENDER"], "");
191
+ let match = Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_PROVIDER"], "");
192
+ if (match === "log") {
193
+ return Messaging_Log_Backend$ReventlessCore.provider(sender);
194
+ } else {
195
+ return Messaging_Ses_Backend$ReventlessAws.provider(sender);
196
+ }
197
+ }
198
+
188
199
  function capabilities() {
189
200
  return {
190
201
  geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined),
191
- messaging: Messaging_Ses_Backend$ReventlessAws.provider(Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_SENDER"], ""))
202
+ messaging: messagingEmailProvider()
192
203
  };
193
204
  }
194
205
 
@@ -290,6 +301,7 @@ export {
290
301
  makePublishJsons,
291
302
  makeSyncTodoItems,
292
303
  makeLoadTodoItems,
304
+ messagingEmailProvider,
293
305
  capabilities,
294
306
  makeAutomationJsonEventsHandler,
295
307
  makeOutboundJsonEventsHandler,
@@ -0,0 +1,141 @@
1
+ // The messaging capability a deployment provisions: which transport, sending as
2
+ // whom. The entry point a platform root calls; the transports sit below it.
3
+ //
4
+ // **All of it is configuration, none of it is code.** Which transport is a
5
+ // per-environment decision — a staging stack should not mail customers, and
6
+ // nothing about the shop it deploys says so — and the address is a per-account
7
+ // fact, since SES sends from one verified identity. A repository cannot know
8
+ // either, and a placeholder default for the address is worse than none: it
9
+ // provisions an identity nobody can verify, after which every send is refused at
10
+ // the sender while reading like a delivery problem.
11
+ //
12
+ // Config keys, on the usual ladder — env var `REVENTLESS_<SCREAMING_SNAKE>`, then
13
+ // `Pulumi.local.yaml`, then `platform:<key>` in `Pulumi.<stack>.yaml`:
14
+ //
15
+ // - `messagingEmailProvider` — "ses" (default) or "log". Per channel, like
16
+ // the senders: an SMS transport will bring its own.
17
+ // - `messagingEmailSender` — the address messages are sent from. Under
18
+ // `ses` it is verified and required; under
19
+ // `log` it is only a header and defaults.
20
+ // - `messagingEmailSenderName` — optional display name for the `From:` header.
21
+ // - `messagingSmsSender` — origination number or sender id. Carried only;
22
+ // no transport reads it yet.
23
+ //
24
+ // **Unset is a real answer, and the deploy still checks it.** A platform whose
25
+ // plugins declare the Messaging capability and which provisions no sender is
26
+ // refused at deploy time by `Platform.deployPlatform`, naming the key — so "no
27
+ // mail configured" cannot reach production as silence.
28
+
29
+ let emailProviderKey = "messagingEmailProvider"
30
+ let emailSenderKey = "messagingEmailSender"
31
+ let emailSenderNameKey = "messagingEmailSenderName"
32
+ let smsSenderKey = "messagingSmsSender"
33
+
34
+ /** One key off the ladder, trimmed, with blank read as absent.
35
+
36
+ Blank matters because the ways to write "nothing" must not differ: a stray
37
+ `REVENTLESS_MESSAGING_EMAIL_SENDER=` in CI, a blank line in
38
+ `Pulumi.local.yaml`, and a key left with a space after the colon all mean the
39
+ stack named no sender. Honouring any of them would ask SES for an identity
40
+ with a blank address — a deploy failure whose cause is whitespace, and one
41
+ that reaches AWS before anything here explains it.
42
+
43
+ Trimmed rather than only compared, so a value that survives is also the value
44
+ used: an address with a trailing space is a `From:` header with one. */
45
+ let configured = (key: string): option<string> =>
46
+ switch Util_LocalConfig.get(key) {
47
+ | Some(_) as v => v
48
+ | None => Pulumi.Config.make(Some("platform"))->Pulumi.Config.get(key)
49
+ }->Option.flatMap(value => {
50
+ let trimmed = value->String.trim
51
+ trimmed === "" ? None : Some(trimmed)
52
+ })
53
+
54
+ /** The transports a deployment can choose between for the EMAIL channel. The name travels to the
55
+ Lambdas, so it is also the wire value — one spelling, not a deploy-time enum
56
+ and a runtime string that can disagree. */
57
+ type emailProvider =
58
+ | Ses
59
+ | Log
60
+
61
+ let emailProviderName = (provider: emailProvider): string =>
62
+ switch provider {
63
+ | Ses => "ses"
64
+ | Log => "log"
65
+ }
66
+
67
+ /**
68
+ Which transport this stack asked for. SES is the default, so a deployment that
69
+ says nothing gets the real one — the safe direction is that mail works, and a
70
+ stack opting out of sending says so explicitly.
71
+
72
+ An unrecognised value is refused rather than defaulted. Falling back to SES on a
73
+ typo would provision a real identity for a stack that asked to only log, which
74
+ is the one mistake this key exists to prevent.
75
+ */
76
+ let parseEmailProvider = (raw: string): emailProvider =>
77
+ switch raw->String.trim->String.toLowerCase {
78
+ | "ses" => Ses
79
+ | "log" => Log
80
+ | other =>
81
+ JsError.throwWithMessage(
82
+ `platform:${emailProviderKey} is "${other}", which is not an email provider.\n` ++
83
+ ` Known values: "ses" (default — a verified SES identity) and "log" ` ++
84
+ `(writes each message to the log, sends nothing).`,
85
+ )
86
+ }
87
+
88
+ let emailProvider = (): emailProvider =>
89
+ configured(emailProviderKey)->Option.mapOr(Ses, parseEmailProvider)
90
+
91
+ /** The `From:` a log-transport deployment presents as when the stack named no
92
+ address.
93
+
94
+ Hard-coded where the SES address is required, and the asymmetry follows from
95
+ what the value is for: a logged message reaches nobody, so there is nothing to
96
+ verify and nothing that differs per environment. `.test` is reserved for
97
+ exactly this (RFC 6761) — it can never resolve, so an address escaping into a
98
+ fixture cannot reach a real inbox. */
99
+ let logDefaultAddress = "notifications@online-shop.test"
100
+
101
+ /**
102
+ Provision the capability and hand back the platform's messaging handle.
103
+
104
+ `~name` is used only by the SES arm, which is the only arm with a resource: the
105
+ log transport is a runtime decision carried in an environment variable, so choosing it
106
+ creates nothing and costs nothing to leave configured on a stack that is torn
107
+ down and rebuilt.
108
+ */
109
+ let make = (
110
+ ~name: string,
111
+ ~opts: option<Pulumi.CustomResourceOptions.t>=?,
112
+ ): ReventlessInfra.Platform.messagingSender => {
113
+ // Read whether or not there is an email sender: the SMS half is independent,
114
+ // and a stack may state one without the other.
115
+ let smsSender = configured(smsSenderKey)->Option.map(Pulumi.Input.make)
116
+ let chosen = emailProvider()
117
+ let displayName = configured(emailSenderNameKey)
118
+ let providerInput = Pulumi.Input.make(chosen->emailProviderName)
119
+
120
+ switch (chosen, configured(emailSenderKey)) {
121
+ // Asked for SES and named no address. Nothing is provisioned, and the deploy
122
+ // gate refuses this the moment a plugin declares it needs messaging — here
123
+ // rather than there because this module has no view of what the plugins want.
124
+ | (Ses, None) => {emailProvider: providerInput, smsSender: ?smsSender}
125
+ | (Ses, Some(address)) => {
126
+ emailSender: Capability_Messaging_Ses.emailSender(~name, ~address, ~displayName, ~opts?),
127
+ emailProvider: providerInput,
128
+ smsSender: ?smsSender,
129
+ }
130
+ | (Log, address) => {
131
+ emailSender: Pulumi.Input.make(
132
+ Reventless.Messaging.fromHeader(
133
+ ~displayName,
134
+ ~address=address->Option.getOr(logDefaultAddress),
135
+ ),
136
+ ),
137
+ emailProvider: providerInput,
138
+ smsSender: ?smsSender,
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,96 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Pulumi from "@pulumi/pulumi";
5
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
7
+ import * as Util_LocalConfig$ReventlessAws from "../util/Util_LocalConfig.res.mjs";
8
+ import * as Capability_Messaging_Ses$ReventlessAws from "./Capability_Messaging_Ses.res.mjs";
9
+
10
+ let emailProviderKey = "messagingEmailProvider";
11
+
12
+ let emailSenderKey = "messagingEmailSender";
13
+
14
+ let emailSenderNameKey = "messagingEmailSenderName";
15
+
16
+ let smsSenderKey = "messagingSmsSender";
17
+
18
+ function configured(key) {
19
+ let v = Util_LocalConfig$ReventlessAws.get(key);
20
+ return Stdlib_Option.flatMap(v !== undefined ? v : new Pulumi.Config("platform").get(key), value => {
21
+ let trimmed = value.trim();
22
+ if (trimmed === "") {
23
+ return;
24
+ } else {
25
+ return trimmed;
26
+ }
27
+ });
28
+ }
29
+
30
+ function emailProviderName(provider) {
31
+ if (provider === "Ses") {
32
+ return "ses";
33
+ } else {
34
+ return "log";
35
+ }
36
+ }
37
+
38
+ function parseEmailProvider(raw) {
39
+ let other = raw.trim().toLowerCase();
40
+ switch (other) {
41
+ case "log" :
42
+ return "Log";
43
+ case "ses" :
44
+ return "Ses";
45
+ default:
46
+ return Stdlib_JsError.throwWithMessage(`platform:` + emailProviderKey + ` is "` + other + `", which is not an email provider.\n Known values: "ses" (default — a verified SES identity) and "log" (writes each message to the log, sends nothing).`);
47
+ }
48
+ }
49
+
50
+ function emailProvider() {
51
+ return Stdlib_Option.mapOr(configured(emailProviderKey), "Ses", parseEmailProvider);
52
+ }
53
+
54
+ let logDefaultAddress = "notifications@online-shop.test";
55
+
56
+ function make(name, opts) {
57
+ let smsSender = Stdlib_Option.map(configured(smsSenderKey), prim => prim);
58
+ let chosen = emailProvider();
59
+ let displayName = configured(emailSenderNameKey);
60
+ let providerInput = emailProviderName(chosen);
61
+ let match = configured(emailSenderKey);
62
+ if (chosen === "Ses") {
63
+ if (match !== undefined) {
64
+ return {
65
+ emailSender: Capability_Messaging_Ses$ReventlessAws.emailSender(name, match, displayName, opts),
66
+ smsSender: smsSender,
67
+ emailProvider: providerInput
68
+ };
69
+ } else {
70
+ return {
71
+ smsSender: smsSender,
72
+ emailProvider: providerInput
73
+ };
74
+ }
75
+ } else {
76
+ return {
77
+ emailSender: Messaging$Reventless.fromHeader(displayName, Stdlib_Option.getOr(match, logDefaultAddress)),
78
+ smsSender: smsSender,
79
+ emailProvider: providerInput
80
+ };
81
+ }
82
+ }
83
+
84
+ export {
85
+ emailProviderKey,
86
+ emailSenderKey,
87
+ emailSenderNameKey,
88
+ smsSenderKey,
89
+ configured,
90
+ emailProviderName,
91
+ parseEmailProvider,
92
+ emailProvider,
93
+ logDefaultAddress,
94
+ make,
95
+ }
96
+ /* @pulumi/pulumi Not a pure module */
@@ -1,10 +1,11 @@
1
- // A verified SES sender identity, provisioned with the framework's house
2
- // conventions applied — the backing for the messaging capability's email channel.
1
+ // A verified SES sender identity the email channel's real transport.
3
2
  //
4
- // The identity is the whole of the provisioning: SES sends from a verified
5
- // address, and everything else a send needs travels on the message. So this
6
- // helper exists to make the sender a deploy-time decision with one owner rather
7
- // than an environment variable each stack spells for itself.
3
+ // Provisioning only. Which transport a deployment uses, and what address it
4
+ // sends from, are read by `Capability_Messaging`, which is the entry point a
5
+ // platform root calls; this module receives a resolved address and creates the
6
+ // identity for it. Split that way because a deployment can choose *not* to have
7
+ // an SES identity, and a module that read its own config could not be left
8
+ // uncalled without also leaving the config unread.
8
9
  //
9
10
  // **Verification is not instant and not automatic.** Creating the identity asks
10
11
  // AWS to send a confirmation mail to the address; until somebody follows it, SES
@@ -18,14 +19,26 @@
18
19
 
19
20
  open PulumiAws
20
21
 
21
- /** Declare the sender address and hand back the platform's messaging handle.
22
- `~email` is the `From:` every message this deployment sends carries. */
23
- let make = (
22
+ /**
23
+ Create the identity and hand back the `From:` it sends as.
24
+
25
+ `~name` is the Pulumi resource name — a URN rather than a setting, so it stays in
26
+ code: moving it to config would replace the resource whenever a stack spelled it
27
+ differently.
28
+
29
+ The identity is created for the bare address, because SES verifies an address and
30
+ not a header; the display name is applied afterwards, to the value the send path
31
+ reads. Applied over the identity's own output rather than over the configured
32
+ string, so the sender a Lambda is handed still depends on the resource existing.
33
+ */
34
+ let emailSender = (
24
35
  ~name: string,
25
- ~email: string,
36
+ ~address: string,
37
+ ~displayName: option<string>,
26
38
  ~opts: option<Pulumi.CustomResourceOptions.t>=?,
27
- ): ReventlessInfra.Platform.messagingSender => {
28
- let identity = SES.EmailIdentity.make(~name, ~args={email: email}, ~opts?)
29
-
30
- {emailSender: identity.email->Pulumi.Output.asInput}
39
+ ): Pulumi.Input.t<string> => {
40
+ let identity = SES.EmailIdentity.make(~name, ~args={email: address}, ~opts?)
41
+ identity.email
42
+ ->Pulumi.Output.apply(verified => Reventless.Messaging.fromHeader(~displayName, ~address=verified))
43
+ ->Pulumi.Output.asInput
31
44
  }
@@ -2,17 +2,16 @@
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
+ import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
5
6
 
6
- function make(name, email, opts) {
7
+ function emailSender(name, address, displayName, opts) {
7
8
  let identity = new (Aws.ses.EmailIdentity)(name, {
8
- email: email
9
+ email: address
9
10
  }, opts !== undefined ? Primitive_option.valFromOption(opts) : undefined);
10
- return {
11
- emailSender: identity.email
12
- };
11
+ return identity.email.apply(verified => Messaging$Reventless.fromHeader(displayName, verified));
13
12
  }
14
13
 
15
14
  export {
16
- make,
15
+ emailSender,
17
16
  }
18
17
  /* @pulumi/aws Not a pure module */
@@ -0,0 +1,100 @@
1
+ open JestGlobals
2
+
3
+ // Which transport a stack mails through, and who it sends as. There is no
4
+ // default address: a placeholder would provision an identity nobody can verify,
5
+ // so "unset" has to survive the whole way to the deploy gate as absence rather
6
+ // than as an address.
7
+
8
+ module Messaging = Capability_Messaging
9
+
10
+ let withEnv = (key, value, f) => {
11
+ Dict.set(NodeProcess.env, key, value)
12
+ let result = f()
13
+ Dict.delete(NodeProcess.env, key)
14
+ result
15
+ }
16
+
17
+ describe("Capability_Messaging — reading a configured value", () => {
18
+ // Only the env-var rung is reachable here: the rungs below it end at
19
+ // `Pulumi.Config`, which needs a deploy-time runtime this suite does not have.
20
+ // `configured` returns before touching it whenever the variable is set.
21
+ testSync("an address set in the environment is the sender", () => {
22
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", "mail@shop.test", () =>
23
+ Messaging.configured(Messaging.emailSenderKey)
24
+ )
25
+ expect(sender)->toEqual(Some("mail@shop.test"))
26
+ })
27
+
28
+ // A stray `REVENTLESS_MESSAGING_EMAIL_SENDER=` in CI, or a blank line in the
29
+ // sidecar. Reading it as an address asks SES for an identity with none.
30
+ testSync("an empty value is not an address", () => {
31
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", "", () =>
32
+ Messaging.configured(Messaging.emailSenderKey)
33
+ )
34
+ expect(sender)->toEqual(None)
35
+ })
36
+
37
+ // The one that is not obviously empty: a key left with a space after the colon.
38
+ // It reached SES as an identity request for " " before this was trimmed.
39
+ testSync("a whitespace-only value is not an address either", () => {
40
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", " ", () =>
41
+ Messaging.configured(Messaging.emailSenderKey)
42
+ )
43
+ expect(sender)->toEqual(None)
44
+ })
45
+
46
+ testSync("a surviving value is the trimmed one", () => {
47
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", " mail@shop.test ", () =>
48
+ Messaging.configured(Messaging.emailSenderKey)
49
+ )
50
+ expect(sender)->toEqual(Some("mail@shop.test"))
51
+ })
52
+
53
+ testSync("the sms sender reads off its own key", () => {
54
+ let sender = withEnv("REVENTLESS_MESSAGING_SMS_SENDER", "+15550100", () =>
55
+ Messaging.configured(Messaging.smsSenderKey)
56
+ )
57
+ expect(sender)->toEqual(Some("+15550100"))
58
+ })
59
+ })
60
+
61
+ describe("Capability_Messaging — choosing a transport", () => {
62
+ testSync("ses and log are the two a stack can name", () => {
63
+ let pair: (Messaging.emailProvider, Messaging.emailProvider) = (Ses, Log)
64
+ expect((Messaging.parseEmailProvider("ses"), Messaging.parseEmailProvider("log")))->toEqual(pair)
65
+ })
66
+
67
+ // Config files are written by hand; a capitalised or padded value means what it
68
+ // says, and refusing it would be pedantry rather than safety.
69
+ testSync("the value is read case- and whitespace-insensitively", () => {
70
+ expect(Messaging.parseEmailProvider(" LOG "))->toEqual(Messaging.Log)
71
+ })
72
+
73
+ // The one that matters: defaulting a typo to SES would provision a real
74
+ // identity for a stack that asked to only log.
75
+ testSync("an unrecognised transport is refused, not defaulted", () => {
76
+ let outcome = try {
77
+ let _ = Messaging.parseEmailProvider("smtp")
78
+ None
79
+ } catch {
80
+ | JsExn(e) => e->JsExn.message
81
+ }
82
+ expect(outcome->Option.getOr("")->String.includes("is not an email provider"))->toBe(true)
83
+ })
84
+
85
+ // SES is the default so that a deployment saying nothing still mails: a stack
86
+ // that means not to send says so, rather than silence meaning it.
87
+ testSync("a named transport wins over the default", () => {
88
+ let chosen = withEnv("REVENTLESS_MESSAGING_EMAIL_PROVIDER", "log", () => Messaging.emailProvider())
89
+ let expected: Messaging.emailProvider = Log
90
+ expect(chosen)->toEqual(expected)
91
+ })
92
+ })
93
+
94
+ describe("Capability_Messaging — the log transport's default sender", () => {
95
+ // Hard-coded where the SES address is required, because a logged message
96
+ // reaches nobody. `.test` can never resolve, so it cannot be a real inbox.
97
+ testSync("is a reserved address", () => {
98
+ expect(Messaging.logDefaultAddress->String.endsWith(".test"))->toBe(true)
99
+ })
100
+ })
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
7
+ import * as Capability_Messaging$ReventlessAws from "../src/capability/Capability_Messaging.res.mjs";
8
+
9
+ function withEnv(key, value, f) {
10
+ process.env[key] = value;
11
+ let result = f();
12
+ Stdlib_Dict.$$delete(process.env, key);
13
+ return result;
14
+ }
15
+
16
+ globalThis.describe("Capability_Messaging — reading a configured value", () => {
17
+ globalThis.test("an address set in the environment is the sender", () => {
18
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", "mail@shop.test", () => Capability_Messaging$ReventlessAws.configured(Capability_Messaging$ReventlessAws.emailSenderKey));
19
+ globalThis.expect(sender).toEqual("mail@shop.test");
20
+ });
21
+ globalThis.test("an empty value is not an address", () => {
22
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", "", () => Capability_Messaging$ReventlessAws.configured(Capability_Messaging$ReventlessAws.emailSenderKey));
23
+ globalThis.expect(sender).toEqual(undefined);
24
+ });
25
+ globalThis.test("a whitespace-only value is not an address either", () => {
26
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", " ", () => Capability_Messaging$ReventlessAws.configured(Capability_Messaging$ReventlessAws.emailSenderKey));
27
+ globalThis.expect(sender).toEqual(undefined);
28
+ });
29
+ globalThis.test("a surviving value is the trimmed one", () => {
30
+ let sender = withEnv("REVENTLESS_MESSAGING_EMAIL_SENDER", " mail@shop.test ", () => Capability_Messaging$ReventlessAws.configured(Capability_Messaging$ReventlessAws.emailSenderKey));
31
+ globalThis.expect(sender).toEqual("mail@shop.test");
32
+ });
33
+ globalThis.test("the sms sender reads off its own key", () => {
34
+ let sender = withEnv("REVENTLESS_MESSAGING_SMS_SENDER", "+15550100", () => Capability_Messaging$ReventlessAws.configured(Capability_Messaging$ReventlessAws.smsSenderKey));
35
+ globalThis.expect(sender).toEqual("+15550100");
36
+ });
37
+ });
38
+
39
+ globalThis.describe("Capability_Messaging — choosing a transport", () => {
40
+ globalThis.test("ses and log are the two a stack can name", () => {
41
+ globalThis.expect([
42
+ Capability_Messaging$ReventlessAws.parseEmailProvider("ses"),
43
+ Capability_Messaging$ReventlessAws.parseEmailProvider("log")
44
+ ]).toEqual([
45
+ "Ses",
46
+ "Log"
47
+ ]);
48
+ });
49
+ globalThis.test("the value is read case- and whitespace-insensitively", () => {
50
+ globalThis.expect(Capability_Messaging$ReventlessAws.parseEmailProvider(" LOG ")).toEqual("Log");
51
+ });
52
+ globalThis.test("an unrecognised transport is refused, not defaulted", () => {
53
+ let outcome;
54
+ try {
55
+ Capability_Messaging$ReventlessAws.parseEmailProvider("smtp");
56
+ outcome = undefined;
57
+ } catch (raw_e) {
58
+ let e = Primitive_exceptions.internalToException(raw_e);
59
+ if (e.RE_EXN_ID === "JsExn") {
60
+ outcome = Stdlib_JsExn.message(e._1);
61
+ } else {
62
+ throw e;
63
+ }
64
+ }
65
+ globalThis.expect(Stdlib_Option.getOr(outcome, "").includes("is not an email provider")).toBe(true);
66
+ });
67
+ globalThis.test("a named transport wins over the default", () => {
68
+ let chosen = withEnv("REVENTLESS_MESSAGING_EMAIL_PROVIDER", "log", () => Capability_Messaging$ReventlessAws.emailProvider());
69
+ globalThis.expect(chosen).toEqual("Log");
70
+ });
71
+ });
72
+
73
+ globalThis.describe("Capability_Messaging — the log transport's default sender", () => {
74
+ globalThis.test("is a reserved address", () => {
75
+ globalThis.expect(Capability_Messaging$ReventlessAws.logDefaultAddress.endsWith(".test")).toBe(true);
76
+ });
77
+ });
78
+
79
+ let Messaging;
80
+
81
+ export {
82
+ Messaging,
83
+ withEnv,
84
+ }
85
+ /* Not a pure module */