@reventlessdev/reventless-aws 3.0.0-alpha.332 → 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.
Files changed (25) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +71 -6
  4. package/src/Platform.res.mjs +63 -12
  5. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +5 -3
  6. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +6 -1
  7. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -1
  8. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +93 -11
  9. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +53 -4
  10. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +53 -11
  11. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +30 -8
  12. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +5 -3
  13. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +6 -1
  14. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +5 -3
  15. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +6 -1
  16. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +3 -1
  17. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +4 -1
  18. package/src/capability/Capability_Messaging.res +141 -0
  19. package/src/capability/Capability_Messaging.res.mjs +96 -0
  20. package/src/capability/Capability_Messaging_Ses.res +27 -14
  21. package/src/capability/Capability_Messaging_Ses.res.mjs +5 -6
  22. package/tests/AutomationSliceEntryPoint_OpsTest.res +122 -0
  23. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +187 -0
  24. package/tests/Capability_MessagingTest.res +100 -0
  25. package/tests/Capability_MessagingTest.res.mjs +85 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
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
+
20
+ # 3.0.0-alpha.333 (2026-09-02)
21
+
22
+ ### Bug Fixes
23
+
24
+ * **automation:** the slice registry rides in the archive, not the environment ([d03b42c](https://github.com/ReventlessDev/reventless-core/commit/d03b42cc074181fb6506f00e40205d617bc70ddb))
25
+
26
+
6
27
  # 3.0.0-alpha.332 (2026-09-01)
7
28
 
8
29
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.332",
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",
23
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
22
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.7",
24
23
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
25
- "@reventlessdev/reventless-core": "3.0.0-alpha.253",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.154",
24
+ "@reventlessdev/reventless-core": "3.0.0-alpha.254",
25
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.155",
26
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
27
27
  "@reventlessdev/reventless-interop": "3.0.0-alpha.34",
28
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.117",
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
  }
@@ -278,9 +278,11 @@ let finish = () =>
278
278
 
279
279
  let handlerConfigOutput =
280
280
  Pulumi.Output.all(handlerOutputs)
281
- ->Pulumi.Output.apply(handlers =>
282
- `{"handlers":[${handlers->Array.join(",")}]}`
283
- )
281
+ ->Pulumi.Output.apply(handlers => {
282
+ let json = `{"handlers":[${handlers->Array.join(",")}]}`
283
+ Util_LambdaEnvBudget.check(~lambdaName="AllAggregatesCmdHandler", ~handlerConfigJson=json)
284
+ json
285
+ })
284
286
 
285
287
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
286
288
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
@@ -14,6 +14,7 @@ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
14
14
  import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
15
15
  import * as EventLogBackend$ReventlessAws from "../EventLog/EventLogBackend.res.mjs";
16
16
  import * as Util_LogAttribution$ReventlessAws from "../../util/Util_LogAttribution.res.mjs";
17
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
17
18
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
18
19
  import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
19
20
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -184,7 +185,11 @@ function finish() {
184
185
  ]).apply(param => `{"specModule":` + specModule + `,"behaviorModule":` + behaviorModule + `,"eventLogTable":"` + param[0] + `","queueUrl":"` + param[1] + `","queueArn":"` + param[2] + `"` + pluginFragment + param[3] + `}`);
185
186
  handlerOutputs.push(handlerJson);
186
187
  });
187
- let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
188
+ let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => {
189
+ let json = `{"handlers":[` + handlers.join(",") + `]}`;
190
+ Util_LambdaEnvBudget$ReventlessAws.check("AllAggregatesCmdHandler", json, undefined);
191
+ return json;
192
+ });
188
193
  let envVars = {};
189
194
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
190
195
  let tableName = pluginRmTableName.contents;
@@ -18,9 +18,21 @@ import { Make as outboundTranslationSliceCallbackMake } from "@reventlessdev/rev
18
18
  import * as StreamOps from "./StreamRoutedEntryPoint_Ops.res.mjs";
19
19
  import * as Ops from "./AutomationSliceEntryPoint_Ops.res.mjs";
20
20
  import { runtimeExtensionsReady } from "./HandlerFactoryHelpers.mjs";
21
+ import { readFileSync } from "node:fs";
22
+ import { join } from "node:path";
21
23
 
22
24
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
23
25
 
26
+ // Absent whenever the config is full-key (tests, and any pre-split deployment),
27
+ // which is why a missing file is not an error.
28
+ const readSliceModules = () => {
29
+ try {
30
+ return readFileSync(join(process.cwd(), "automationSliceModules.json"), "utf-8");
31
+ } catch {
32
+ return "";
33
+ }
34
+ };
35
+
24
36
  async function buildAllHandlers() {
25
37
  // Runtime extension seam: any registered out-of-tree extension gets its
26
38
  // onColdStart before a single handler is built, which is what makes the
@@ -29,7 +41,15 @@ async function buildAllHandlers() {
29
41
  await runtimeExtensionsReady;
30
42
  const handlers = {};
31
43
  const sweeps = [];
32
- const entries = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
44
+ // The slice registry rides in the archive, not the environment — two module
45
+ // specifiers per slice outgrow Lambda's 4KB env-var ceiling (see the asset in
46
+ // AutomationSliceRuntime_Builder_Single). A full-key config still wins, so a
47
+ // caller that supplies one — every test that drives a config directly — needs
48
+ // no asset on disk. process.cwd() is /var/task, where the archive unpacks.
49
+ const entries = Ops.parseHandlerConfigWithModules(
50
+ readSliceModules(),
51
+ process.env["HANDLER_CONFIG"] || "",
52
+ );
33
53
 
34
54
  await Promise.all(entries.map(async (entry) => {
35
55
  if (!entry.bodyModule) {
@@ -62,16 +62,83 @@ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
62
62
  context: h->Dict.get("context")->Option.flatMap(decodeContext),
63
63
  })
64
64
 
65
- let parseHandlerConfig = (rawJson: string): array<handlerEntry> =>
65
+ // A compact entry names its slice and carries only what the deploy resolved;
66
+ // the module paths and context live in the archive registry, keyed by the same
67
+ // name (AutomationSliceRuntime_Builder_Single). Shared `queueUrl` /
68
+ // `commandQueueIsFifo` / `urns` are hoisted out of the per-entry keys, since a
69
+ // slice publishing to the DCB fallback and reading its plugin's log repeats
70
+ // both verbatim — which is what outgrew the 4KB env limit.
71
+ let decodeCompactEntry = (
72
+ ~modules: dict<JSON.t>,
73
+ ~sharedQueueUrl: string,
74
+ ~sharedIsFifo: option<bool>,
75
+ ~sharedUrns: array<string>,
76
+ h: dict<JSON.t>,
77
+ name: string,
78
+ ): handlerEntry => {
79
+ let m = modules->Dict.get(name)->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
80
+ let urns =
81
+ h
82
+ ->Dict.get("u")
83
+ ->Option.flatMap(JSON.Decode.array)
84
+ ->Option.map(a => a->Array.filterMap(JSON.Decode.string))
85
+ ->Option.getOr(sharedUrns)
86
+ {
87
+ specModule: m->strOf("specModule")->Option.getOr(""),
88
+ bodyModule: m->strOf("bodyModule")->Option.getOr(""),
89
+ callbackType: m->strOf("callbackType")->Option.getOr("automation"),
90
+ queryDbTableName: h->strOf("q")->Option.getOr(""),
91
+ dcbQueueUrl: h->strOf("k")->Option.getOr(sharedQueueUrl),
92
+ commandQueueIsFifo: switch h->Dict.get("f")->Option.flatMap(JSON.Decode.bool) {
93
+ | Some(f) => Some(f)
94
+ | None => sharedIsFifo
95
+ },
96
+ // Only the first, which is all the shell registers today. The remaining
97
+ // urns stay in the config for the multi-source registration to use.
98
+ sourceUrn: urns->Array.get(0)->Option.getOr(""),
99
+ context: m->Dict.get("context")->Option.flatMap(decodeContext),
100
+ }
101
+ }
102
+
103
+ /** `modulesJson` is the archive-side slice registry (`""` when there is none);
104
+ an entry carrying its own `specModule` is a full-key config and needs it. */
105
+ let parseHandlerConfigWithModules = (modulesJson: string, rawJson: string): array<handlerEntry> =>
66
106
  rawJson == ""
67
107
  ? []
68
- : rawJson
69
- ->JSON.parseOrThrow
70
- ->JSON.Decode.object
71
- ->Option.flatMap(obj => obj->Dict.get("handlers"))
72
- ->Option.flatMap(JSON.Decode.array)
73
- ->Option.getOr([])
74
- ->Array.filterMap(decodeEntry)
108
+ : {
109
+ let obj = rawJson->JSON.parseOrThrow->JSON.Decode.object->Option.getOr(Dict.make())
110
+ let modules =
111
+ modulesJson == ""
112
+ ? Dict.make()
113
+ : modulesJson->JSON.parseOrThrow->JSON.Decode.object->Option.getOr(Dict.make())
114
+ let sharedQueueUrl = obj->strOf("queueUrl")->Option.getOr("")
115
+ let sharedIsFifo = obj->Dict.get("commandQueueIsFifo")->Option.flatMap(JSON.Decode.bool)
116
+ let sharedUrns =
117
+ obj
118
+ ->Dict.get("urns")
119
+ ->Option.flatMap(JSON.Decode.array)
120
+ ->Option.map(a => a->Array.filterMap(JSON.Decode.string))
121
+ ->Option.getOr([])
122
+ obj
123
+ ->Dict.get("handlers")
124
+ ->Option.flatMap(JSON.Decode.array)
125
+ ->Option.getOr([])
126
+ ->Array.filterMap(item =>
127
+ switch item->JSON.Decode.object {
128
+ | None => None
129
+ | Some(h) =>
130
+ switch h->strOf("n") {
131
+ | Some(name) =>
132
+ Some(decodeCompactEntry(~modules, ~sharedQueueUrl, ~sharedIsFifo, ~sharedUrns, h, name))
133
+ | None => decodeEntry(item)
134
+ }
135
+ }
136
+ )
137
+ }
138
+
139
+ /** The full-key form, for a config that carries its own module paths. */
140
+ let parseHandlerConfig = (rawJson: string): array<handlerEntry> =>
141
+ parseHandlerConfigWithModules("", rawJson)
75
142
 
76
143
  // A pre-`bodyModule` config entry cannot rebuild the callback (the spec module
77
144
  // alone lacks the mappings/process). Warn and skip rather than crash the whole
@@ -230,15 +297,30 @@ outcome, not a verdict on the address. `MESSAGING_EMAIL_SENDER` is read the same
230
297
  way, and empty yields a provider with no channels rather than one that claims a
231
298
  channel it cannot send on.
232
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
+
233
317
  let capabilities = (): Reventless.Capabilities.t => {
234
318
  geocode: (~text) =>
235
319
  Geocoder_AwsLocation_Backend.search(
236
320
  ~indexName=NodeProcess.env->Dict.get("PLACE_INDEX_NAME")->Option.getOr(""),
237
321
  ~text,
238
322
  ),
239
- messaging: Messaging_Ses_Backend.provider(
240
- ~sender=NodeProcess.env->Dict.get("MESSAGING_EMAIL_SENDER")->Option.getOr(""),
241
- ),
323
+ messaging: messagingEmailProvider(),
242
324
  }
243
325
 
244
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";
@@ -49,12 +50,47 @@ function decodeEntry(json) {
49
50
  }));
50
51
  }
51
52
 
52
- function parseHandlerConfig(rawJson) {
53
+ function decodeCompactEntry(modules, sharedQueueUrl, sharedIsFifo, sharedUrns, h, name) {
54
+ let m = Stdlib_Option.getOr(Stdlib_Option.flatMap(modules[name], Stdlib_JSON.Decode.object), {});
55
+ let urns = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(h["u"], Stdlib_JSON.Decode.array), a => Stdlib_Array.filterMap(a, Stdlib_JSON.Decode.string)), sharedUrns);
56
+ let f = Stdlib_Option.flatMap(h["f"], Stdlib_JSON.Decode.bool);
57
+ return {
58
+ specModule: Stdlib_Option.getOr(strOf(m, "specModule"), ""),
59
+ bodyModule: Stdlib_Option.getOr(strOf(m, "bodyModule"), ""),
60
+ callbackType: Stdlib_Option.getOr(strOf(m, "callbackType"), "automation"),
61
+ queryDbTableName: Stdlib_Option.getOr(strOf(h, "q"), ""),
62
+ dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "k"), sharedQueueUrl),
63
+ commandQueueIsFifo: f !== undefined ? f : sharedIsFifo,
64
+ sourceUrn: Stdlib_Option.getOr(urns[0], ""),
65
+ context: Stdlib_Option.flatMap(m["context"], decodeContext)
66
+ };
67
+ }
68
+
69
+ function parseHandlerConfigWithModules(modulesJson, rawJson) {
53
70
  if (rawJson === "") {
54
71
  return [];
55
- } else {
56
- return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(rawJson)), obj => obj["handlers"]), Stdlib_JSON.Decode.array), []), decodeEntry);
57
72
  }
73
+ let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(rawJson)), {});
74
+ let modules = modulesJson === "" ? ({}) : Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(modulesJson)), {});
75
+ let sharedQueueUrl = Stdlib_Option.getOr(strOf(obj, "queueUrl"), "");
76
+ let sharedIsFifo = Stdlib_Option.flatMap(obj["commandQueueIsFifo"], Stdlib_JSON.Decode.bool);
77
+ let sharedUrns = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(obj["urns"], Stdlib_JSON.Decode.array), a => Stdlib_Array.filterMap(a, Stdlib_JSON.Decode.string)), []);
78
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["handlers"], Stdlib_JSON.Decode.array), []), item => {
79
+ let h = Stdlib_JSON.Decode.object(item);
80
+ if (h === undefined) {
81
+ return;
82
+ }
83
+ let name = strOf(h, "n");
84
+ if (name !== undefined) {
85
+ return decodeCompactEntry(modules, sharedQueueUrl, sharedIsFifo, sharedUrns, h, name);
86
+ } else {
87
+ return decodeEntry(item);
88
+ }
89
+ });
90
+ }
91
+
92
+ function parseHandlerConfig(rawJson) {
93
+ return parseHandlerConfigWithModules("", rawJson);
58
94
  }
59
95
 
60
96
  function warnMissingBodyModule(entry) {
@@ -150,10 +186,20 @@ function makeLoadTodoItems(queryDbTableName, todoItems, rowSchema, comp) {
150
186
  };
151
187
  }
152
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
+
153
199
  function capabilities() {
154
200
  return {
155
201
  geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined),
156
- messaging: Messaging_Ses_Backend$ReventlessAws.provider(Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_SENDER"], ""))
202
+ messaging: messagingEmailProvider()
157
203
  };
158
204
  }
159
205
 
@@ -247,12 +293,15 @@ export {
247
293
  strOf,
248
294
  decodeContext,
249
295
  decodeEntry,
296
+ decodeCompactEntry,
297
+ parseHandlerConfigWithModules,
250
298
  parseHandlerConfig,
251
299
  warnMissingBodyModule,
252
300
  defaultContext,
253
301
  makePublishJsons,
254
302
  makeSyncTodoItems,
255
303
  makeLoadTodoItems,
304
+ messagingEmailProvider,
256
305
  capabilities,
257
306
  makeAutomationJsonEventsHandler,
258
307
  makeOutboundJsonEventsHandler,