@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 +14 -0
- package/package.json +7 -7
- package/src/Platform.res +71 -6
- package/src/Platform.res.mjs +63 -12
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +18 -3
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +13 -1
- package/src/capability/Capability_Messaging.res +141 -0
- package/src/capability/Capability_Messaging.res.mjs +96 -0
- package/src/capability/Capability_Messaging_Ses.res +27 -14
- package/src/capability/Capability_Messaging_Ses.res.mjs +5 -6
- package/tests/Capability_MessagingTest.res +100 -0
- package/tests/Capability_MessagingTest.res.mjs +85 -0
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.
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
`
|
|
2190
|
-
|
|
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 \`
|
|
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
|
package/src/Platform.res.mjs
CHANGED
|
@@ -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
|
-
|
|
1172
|
-
|
|
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$
|
|
1244
|
-
if (componentDefinitions !== undefined && match$
|
|
1245
|
-
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$
|
|
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 \`
|
|
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
|
-
|
|
2510
|
-
|
|
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$
|
|
2582
|
-
if (componentDefinitions !== undefined && match$
|
|
2583
|
-
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$
|
|
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 \`
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
~
|
|
36
|
+
~address: string,
|
|
37
|
+
~displayName: option<string>,
|
|
26
38
|
~opts: option<Pulumi.CustomResourceOptions.t>=?,
|
|
27
|
-
):
|
|
28
|
-
let identity = SES.EmailIdentity.make(~name, ~args={email:
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
7
|
+
function emailSender(name, address, displayName, opts) {
|
|
7
8
|
let identity = new (Aws.ses.EmailIdentity)(name, {
|
|
8
|
-
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
|
-
|
|
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 */
|