belt-messaging 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ # Messaging Module — SMS via AWS PinpointSMSVoiceV2, Email via AWS SESv2
2
+ # Generated by: belt generate messaging
3
+ #
4
+ # Provides:
5
+ # - Toll-free number provisioning (or shared number configuration)
6
+ # - Two-way messaging (inbound SMS → SNS → Lambda)
7
+ # - IAM policy for sending SMS
8
+ # - Opt-in keyword configuration (START/UNSTOP)
9
+ # - SES email identity verification
10
+ # - IAM policy for sending email
11
+
12
+ terraform {
13
+ required_providers {
14
+ aws = {
15
+ source = "hashicorp/aws"
16
+ version = "~> 5.0"
17
+ }
18
+ }
19
+ }
20
+
21
+ locals {
22
+ common_tags = {
23
+ Module = "messaging"
24
+ Environment = var.environment
25
+ ManagedBy = "Terraform"
26
+ }
27
+
28
+ # Safe three-way resolution: domain takes precedence, then address, then empty.
29
+ # If enable_email is true but neither identity var is set, this resolves to ""
30
+ # and the email_send policy is skipped (count gated on this being non-empty).
31
+ email_identity_arn = var.enable_email ? (
32
+ var.email_domain != "" ? aws_sesv2_email_identity.domain[0].arn : (
33
+ var.email_from_address != "" ? aws_sesv2_email_identity.address[0].arn : ""
34
+ )
35
+ ) : ""
36
+ }
37
+
38
+ # ============================================================================
39
+ # SNS Topic for Two-Way Messaging (Inbound SMS)
40
+ # ============================================================================
41
+
42
+ resource "aws_sns_topic" "sms_inbound" {
43
+ count = var.enable_sms ? 1 : 0
44
+
45
+ name = "${var.app_name}-${var.environment}-sms-inbound"
46
+ tags = merge(local.common_tags, {
47
+ Name = "${var.app_name}-${var.environment}-sms-inbound"
48
+ })
49
+ }
50
+
51
+ # Allow Pinpoint SMS/Voice v2 to publish to the inbound SNS topic
52
+ resource "aws_sns_topic_policy" "sms_inbound" {
53
+ count = var.enable_sms ? 1 : 0
54
+
55
+ arn = aws_sns_topic.sms_inbound[0].arn
56
+ policy = jsonencode({
57
+ Version = "2012-10-17"
58
+ Statement = [{
59
+ Sid = "AllowPinpointSMSPublish"
60
+ Effect = "Allow"
61
+ Principal = { Service = "sms-voice.amazonaws.com" }
62
+ Action = "sns:Publish"
63
+ Resource = aws_sns_topic.sms_inbound[0].arn
64
+ }]
65
+ })
66
+ }
67
+
68
+ # ============================================================================
69
+ # Toll-Free Number (Optional — Production)
70
+ # ============================================================================
71
+ # Toll-Free Verification (TFV) must be completed manually in the AWS console
72
+ # after provisioning. Number stays PENDING until approved (2-15 business days).
73
+ #
74
+ # Recovery after Terraform timeout:
75
+ # 1. terraform state rm 'module.messaging.aws_pinpointsmsvoicev2_phone_number.toll_free[0]'
76
+ # 2. terraform import 'module.messaging.aws_pinpointsmsvoicev2_phone_number.toll_free[0]' phone-XXXXX
77
+ # 3. Complete TFV registration in the End User Messaging console
78
+
79
+ resource "aws_pinpointsmsvoicev2_phone_number" "toll_free" {
80
+ count = var.request_toll_free_number ? 1 : 0
81
+
82
+ iso_country_code = "US"
83
+ message_type = "TRANSACTIONAL"
84
+ number_type = "TOLL_FREE"
85
+ number_capabilities = ["SMS"]
86
+
87
+ deletion_protection_enabled = true
88
+
89
+ two_way_channel_arn = var.enable_sms ? aws_sns_topic.sms_inbound[0].arn : null
90
+ two_way_channel_enabled = var.enable_sms
91
+ self_managed_opt_outs_enabled = var.enable_sms
92
+
93
+ timeouts {
94
+ create = "2m"
95
+ }
96
+
97
+ tags = merge(local.common_tags, {
98
+ Name = "${var.app_name}-${var.environment}-sms-toll-free"
99
+ })
100
+ }
101
+
102
+ # Configure START/UNSTOP keywords for opt-in on the toll-free number
103
+ resource "terraform_data" "toll_free_opt_in_keywords" {
104
+ count = var.request_toll_free_number && var.enable_sms ? 1 : 0
105
+
106
+ triggers_replace = [aws_pinpointsmsvoicev2_phone_number.toll_free[0].id]
107
+
108
+ provisioner "local-exec" {
109
+ command = <<-EOT
110
+ aws pinpoint-sms-voice-v2 put-keyword \
111
+ --origination-identity ${aws_pinpointsmsvoicev2_phone_number.toll_free[0].id} \
112
+ --keyword START --keyword-message "${var.opt_in_message}" \
113
+ --keyword-action OPT_IN --region ${var.aws_region} && \
114
+ aws pinpoint-sms-voice-v2 put-keyword \
115
+ --origination-identity ${aws_pinpointsmsvoicev2_phone_number.toll_free[0].id} \
116
+ --keyword UNSTOP --keyword-message "${var.opt_in_message}" \
117
+ --keyword-action OPT_IN --region ${var.aws_region}
118
+ EOT
119
+ }
120
+ }
121
+
122
+ # ============================================================================
123
+ # Shared Number Two-Way Configuration (Dev/Staging)
124
+ # ============================================================================
125
+ # When using a pre-existing shared toll-free number, configure two-way messaging
126
+ # so inbound STOP/START messages reach this environment's SNS topic.
127
+ #
128
+ # NOTE: Only one environment can "own" two-way on a shared number at a time.
129
+ # The last to apply wins. Fine for dev; production should own its number.
130
+
131
+ resource "terraform_data" "shared_number_two_way" {
132
+ count = var.enable_sms && !var.request_toll_free_number && var.sms_origination_number != "" ? 1 : 0
133
+
134
+ triggers_replace = [var.sms_origination_number, aws_sns_topic.sms_inbound[0].arn]
135
+
136
+ provisioner "local-exec" {
137
+ command = <<-EOT
138
+ PHONE_ID=$(aws pinpoint-sms-voice-v2 describe-phone-numbers \
139
+ --region ${var.aws_region} --output json \
140
+ | jq -r '.PhoneNumbers[] | select(.PhoneNumber=="${var.sms_origination_number}") | .PhoneNumberId') && \
141
+ [ -n "$PHONE_ID" ] && \
142
+ aws pinpoint-sms-voice-v2 update-phone-number \
143
+ --phone-number-id "$PHONE_ID" \
144
+ --two-way-enabled \
145
+ --two-way-channel-arn ${aws_sns_topic.sms_inbound[0].arn} \
146
+ --self-managed-opt-outs-enabled \
147
+ --region ${var.aws_region}
148
+ EOT
149
+ }
150
+ }
151
+
152
+ # Configure START/UNSTOP keywords on the shared number
153
+ resource "terraform_data" "shared_number_opt_in_keywords" {
154
+ count = var.enable_sms && !var.request_toll_free_number && var.sms_origination_number != "" ? 1 : 0
155
+
156
+ triggers_replace = [var.sms_origination_number]
157
+
158
+ provisioner "local-exec" {
159
+ command = <<-EOT
160
+ PHONE_ID=$(aws pinpoint-sms-voice-v2 describe-phone-numbers \
161
+ --region ${var.aws_region} --output json \
162
+ | jq -r '.PhoneNumbers[] | select(.PhoneNumber=="${var.sms_origination_number}") | .PhoneNumberId') && \
163
+ [ -n "$PHONE_ID" ] && \
164
+ aws pinpoint-sms-voice-v2 put-keyword \
165
+ --origination-identity "$PHONE_ID" \
166
+ --keyword START --keyword-message "${var.opt_in_message}" \
167
+ --keyword-action OPT_IN --region ${var.aws_region} && \
168
+ aws pinpoint-sms-voice-v2 put-keyword \
169
+ --origination-identity "$PHONE_ID" \
170
+ --keyword UNSTOP --keyword-message "${var.opt_in_message}" \
171
+ --keyword-action OPT_IN --region ${var.aws_region}
172
+ EOT
173
+ }
174
+ }
175
+
176
+ # ============================================================================
177
+ # IAM Policy — SMS Send Permission
178
+ # ============================================================================
179
+
180
+ data "aws_iam_policy_document" "sms_send" {
181
+ count = var.enable_sms ? 1 : 0
182
+
183
+ statement {
184
+ effect = "Allow"
185
+ actions = [
186
+ "sms-voice:SendTextMessage"
187
+ ]
188
+ resources = ["*"]
189
+ }
190
+ }
191
+
192
+ resource "aws_iam_policy" "sms_send" {
193
+ count = var.enable_sms ? 1 : 0
194
+
195
+ name = "${var.app_name}-${var.environment}-sms-send"
196
+ description = "Allow Lambda to send SMS via PinpointSMSVoiceV2"
197
+ policy = data.aws_iam_policy_document.sms_send[0].json
198
+
199
+ tags = local.common_tags
200
+ }
201
+
202
+ # ============================================================================
203
+ # SES Email Identity
204
+ # ============================================================================
205
+ # When enable_email is true, we verify either a domain or specific email address
206
+ # for sending transactional email.
207
+ #
208
+ # Domain verification (recommended for production):
209
+ # Set email_domain = "example.com" and add the generated DNS records.
210
+ #
211
+ # Email address verification (quick setup):
212
+ # Set email_from_address = "noreply@example.com" — a verification email is sent.
213
+
214
+ resource "aws_sesv2_email_identity" "domain" {
215
+ count = var.enable_email && var.email_domain != "" ? 1 : 0
216
+
217
+ email_identity = var.email_domain
218
+
219
+ tags = merge(local.common_tags, {
220
+ Name = "${var.app_name}-${var.environment}-email-domain"
221
+ })
222
+ }
223
+
224
+ resource "aws_sesv2_email_identity" "address" {
225
+ count = var.enable_email && var.email_domain == "" && var.email_from_address != "" ? 1 : 0
226
+
227
+ email_identity = var.email_from_address
228
+
229
+ tags = merge(local.common_tags, {
230
+ Name = "${var.app_name}-${var.environment}-email-address"
231
+ })
232
+ }
233
+
234
+ # ============================================================================
235
+ # IAM Policy — Email Send Permission
236
+ # ============================================================================
237
+
238
+ check "email_identity_configured" {
239
+ assert {
240
+ condition = !var.enable_email || var.email_domain != "" || var.email_from_address != ""
241
+ error_message = "enable_email is true but neither email_domain nor email_from_address is set. No email identity will be created and sending will not work."
242
+ }
243
+ }
244
+
245
+ data "aws_iam_policy_document" "email_send" {
246
+ count = var.enable_email && local.email_identity_arn != "" ? 1 : 0
247
+
248
+ statement {
249
+ effect = "Allow"
250
+ actions = [
251
+ "ses:SendEmail",
252
+ "ses:SendRawEmail" # Forward-looking: supports future attachment/raw MIME use cases
253
+ ]
254
+ resources = [local.email_identity_arn]
255
+ }
256
+ }
257
+
258
+ resource "aws_iam_policy" "email_send" {
259
+ count = var.enable_email && local.email_identity_arn != "" ? 1 : 0
260
+
261
+ name = "${var.app_name}-${var.environment}-email-send"
262
+ description = "Allow Lambda to send email via SESv2"
263
+ policy = data.aws_iam_policy_document.email_send[0].json
264
+
265
+ tags = local.common_tags
266
+ }
@@ -0,0 +1,52 @@
1
+ # Generated by: belt generate messaging
2
+
3
+ # ============================================================================
4
+ # SMS Outputs
5
+ # ============================================================================
6
+
7
+ output "sms_inbound_topic_arn" {
8
+ description = "ARN of SNS topic for inbound SMS events (two-way messaging)"
9
+ value = var.enable_sms ? aws_sns_topic.sms_inbound[0].arn : ""
10
+ }
11
+
12
+ output "toll_free_number" {
13
+ description = "Toll-free phone number provisioned for SMS origination"
14
+ value = var.request_toll_free_number ? aws_pinpointsmsvoicev2_phone_number.toll_free[0].phone_number : ""
15
+ }
16
+
17
+ output "sms_send_policy_arn" {
18
+ description = "ARN of IAM policy granting sms-voice:SendTextMessage"
19
+ value = var.enable_sms ? aws_iam_policy.sms_send[0].arn : ""
20
+ }
21
+
22
+ # ============================================================================
23
+ # Email Outputs
24
+ # ============================================================================
25
+
26
+ output "email_identity" {
27
+ description = "The verified email identity (domain or address)"
28
+ value = var.enable_email ? (
29
+ var.email_domain != "" ? var.email_domain : var.email_from_address
30
+ ) : ""
31
+ }
32
+
33
+ output "email_identity_verification_status" {
34
+ description = "Verification status of the email identity"
35
+ value = var.enable_email ? (
36
+ var.email_domain != "" ? aws_sesv2_email_identity.domain[0].verified_for_sending_status : (
37
+ var.email_from_address != "" ? aws_sesv2_email_identity.address[0].verified_for_sending_status : false
38
+ )
39
+ ) : false
40
+ }
41
+
42
+ output "email_send_policy_arn" {
43
+ description = "ARN of IAM policy granting ses:SendEmail"
44
+ value = var.enable_email && local.email_identity_arn != "" ? aws_iam_policy.email_send[0].arn : ""
45
+ }
46
+
47
+ output "email_domain_dkim_tokens" {
48
+ description = "DKIM tokens for DNS configuration (only for domain verification)"
49
+ value = var.enable_email && var.email_domain != "" ? (
50
+ aws_sesv2_email_identity.domain[0].dkim_signing_attributes[0].tokens
51
+ ) : []
52
+ }
@@ -0,0 +1,66 @@
1
+ # Generated by: belt generate messaging
2
+
3
+ variable "app_name" {
4
+ description = "Application name"
5
+ type = string
6
+ }
7
+
8
+ variable "environment" {
9
+ description = "Environment name (dev01, staging, prod)"
10
+ type = string
11
+ }
12
+
13
+ variable "aws_region" {
14
+ description = "AWS region"
15
+ type = string
16
+ }
17
+
18
+ # ============================================================================
19
+ # SMS Configuration
20
+ # ============================================================================
21
+
22
+ variable "enable_sms" {
23
+ description = "Enable SMS messaging"
24
+ type = bool
25
+ default = true
26
+ }
27
+
28
+ variable "request_toll_free_number" {
29
+ description = "Provision a new toll-free number (requires TFV registration, 2-15 day approval)"
30
+ type = bool
31
+ default = false
32
+ }
33
+
34
+ variable "sms_origination_number" {
35
+ description = "Pre-existing origination number in E.164 format (for shared dev environments)"
36
+ type = string
37
+ default = ""
38
+ }
39
+
40
+ variable "opt_in_message" {
41
+ description = "Auto-reply message when user texts START/UNSTOP to opt back in"
42
+ type = string
43
+ default = "You have been re-subscribed to SMS notifications."
44
+ }
45
+
46
+ # ============================================================================
47
+ # Email Configuration
48
+ # ============================================================================
49
+
50
+ variable "enable_email" {
51
+ description = "Enable email messaging via SES"
52
+ type = bool
53
+ default = false
54
+ }
55
+
56
+ variable "email_domain" {
57
+ description = "Domain to verify for sending email (e.g., 'example.com'). Takes precedence over email_from_address."
58
+ type = string
59
+ default = ""
60
+ }
61
+
62
+ variable "email_from_address" {
63
+ description = "Email address to verify for sending (e.g., 'noreply@example.com'). Used only if email_domain is not set."
64
+ type = string
65
+ default = ""
66
+ }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Messaging
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'messaging/version'
4
+ require_relative 'messaging/configuration'
5
+ require_relative 'messaging/logging'
6
+ require_relative 'messaging/phone_formatter'
7
+ require_relative 'messaging/sms_gate'
8
+ require_relative 'messaging/sms'
9
+ require_relative 'messaging/email_gate'
10
+ require_relative 'messaging/email'
11
+
12
+ module Belt
13
+ module Messaging
14
+ class Error < StandardError; end
15
+ class ConfigurationError < Error; end
16
+
17
+ extend Logging
18
+
19
+ class << self
20
+ attr_writer :configuration
21
+
22
+ # Public logging interface — delegates to the Logging module.
23
+ # Used by controllers and available to consumer apps.
24
+ public :log
25
+
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ def configure
31
+ yield(configuration)
32
+ end
33
+
34
+ def reset_configuration!
35
+ @configuration = Configuration.new
36
+ end
37
+
38
+ # Convenience: send an SMS
39
+ # @param to [String] Phone number (any US format — formatted to E.164 automatically)
40
+ # @param message [String] Message body
41
+ # @return [Boolean] true if sent successfully
42
+ def send_sms(to:, message:)
43
+ SMS.send(to: to, message: message)
44
+ end
45
+
46
+ # Convenience: send an email
47
+ # @param to [String, Array<String>] Email address(es)
48
+ # @param subject [String] Email subject
49
+ # @param body [String] Email body (plain text)
50
+ # @param html [String, nil] Optional HTML body
51
+ # @param from [String, nil] Override configured from address
52
+ # @return [Boolean] true if sent successfully
53
+ def send_email(to:, subject:, body:, html: nil, from: nil)
54
+ Email.send(to: to, subject: subject, body: body, html: html, from: from)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'belt/messaging'
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: belt-messaging
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stowzilla
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: belt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: aws-sdk-pinpointsmsvoicev2
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: aws-sdk-sesv2
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ description: Belt plugin providing SMS and email messaging. SMS via AWS PinpointSMSVoiceV2,
55
+ email via AWS SESv2. Includes phone verification, opt-in/opt-out handling, and Terraform
56
+ infrastructure.
57
+ email:
58
+ - andy@stowzilla.com
59
+ - adam@stowzilla.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - CHANGELOG.md
65
+ - LICENSE
66
+ - README.md
67
+ - lib/belt-messaging.rb
68
+ - lib/belt/generators/messaging_generator.rb
69
+ - lib/belt/messaging.rb
70
+ - lib/belt/messaging/configuration.rb
71
+ - lib/belt/messaging/controllers/messaging_events_controller.rb
72
+ - lib/belt/messaging/controllers/sms_verification_controller.rb
73
+ - lib/belt/messaging/email.rb
74
+ - lib/belt/messaging/email_gate.rb
75
+ - lib/belt/messaging/logging.rb
76
+ - lib/belt/messaging/phone_formatter.rb
77
+ - lib/belt/messaging/sms.rb
78
+ - lib/belt/messaging/sms_gate.rb
79
+ - lib/belt/messaging/templates/config/messaging_events.yml.erb
80
+ - lib/belt/messaging/templates/controllers/messaging_events_controller.rb.erb
81
+ - lib/belt/messaging/templates/controllers/sms_verification_controller.rb.erb
82
+ - lib/belt/messaging/templates/lambda/messaging_events.rb.erb
83
+ - lib/belt/messaging/templates/terraform/main.tf.erb
84
+ - lib/belt/messaging/templates/terraform/outputs.tf.erb
85
+ - lib/belt/messaging/templates/terraform/variables.tf.erb
86
+ - lib/belt/messaging/version.rb
87
+ homepage: https://github.com/stowzilla/belt-messaging
88
+ licenses:
89
+ - MIT
90
+ metadata:
91
+ homepage_uri: https://github.com/stowzilla/belt-messaging
92
+ source_code_uri: https://github.com/stowzilla/belt-messaging
93
+ changelog_uri: https://github.com/stowzilla/belt-messaging/blob/main/CHANGELOG.md
94
+ rubygems_mfa_required: 'true'
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '3.3'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.6.9
110
+ specification_version: 4
111
+ summary: SMS and email messaging for Belt applications via AWS
112
+ test_files: []