studio-engine 0.5.6 → 0.5.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dde49831782823962437f0efab2c8334fe0efc4376873ee9b9c4c2486aa4d1f
4
- data.tar.gz: c9cb4750ad4298869623db8a98e93662b101a1b2ce055fb779d15a3095ba1529
3
+ metadata.gz: 401a5db38e438ea4335d910bc67c88559739cb227a7377503c152cb2cea409ba
4
+ data.tar.gz: 12703eff81b8af83d9d73e26ebfbd7ec11b6a53444372c8330f32f7b09b89113
5
5
  SHA512:
6
- metadata.gz: bb82ea299130783431b2dfea4fac8ddece0728b7f49d6ea15c1cff49d819fb6687a5a03f4eb80d4a0f8cb590cf78e45ccb1cdec410bc042ba6caed95bdde24b0
7
- data.tar.gz: e93039393ac8e344f7eef097ce55629f41cdefdb8f48be418ad3f8a7228e1c01363a3646ecd23708cffa182d3e905e17a8c1b2f8eccbfe6b97a3c9ef203a855b
6
+ metadata.gz: 8eff2ade70b7a335ac0f50a15e2085fa05a0ba4d5ae7ff36ff7a03c8ec811022fb4d6ece26fed7639c9faf3431d66a1548cc86ed0f261cf255d2dcd4c84ad51e
7
+ data.tar.gz: 72df2ccff7e151d391edd34c62d1a1e1ac1782317a14548f0fcfd541e58646dba89fc13ea254a1a141d1347d9016ee42e04b7c475a426eb9c8eb959725c6a588
data/CHANGELOG.md CHANGED
@@ -4,7 +4,32 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
- No entries yet.
7
+ ## v0.5.8 (2026-06-14)
8
+
9
+ ### Added
10
+ - **`StudioEmailDeliveryHelper#email_delivery_banner_status`** — shared
11
+ non-production banner status text for whether the current process sends
12
+ external email and which transport is active (`resend`, `ses`, `capture`, or
13
+ the ActionMailer delivery method).
14
+
15
+ ## v0.5.7 (2026-06-14)
16
+
17
+ ### Added
18
+ - **`Studio.mailer_from_for_transport` / `Studio.marketing_from_for_transport`** —
19
+ provider-aware sender helpers. SES-ready apps use app/domain-specific
20
+ `MAILER_FROM` and `MARKETING_MAILER_FROM`; Resend fallback uses
21
+ `RESEND_MAILER_FROM`, defaulting to `McRitchie Studio <team@mcritchie.studio>`
22
+ so new apps can send during SES sandbox/presetup without verifying a second
23
+ Resend domain.
24
+
25
+ ### Fixed
26
+ - **`ses:check` / `ses:verify_domain` credential selection** now prefers
27
+ `SES_AWS_ACCESS_KEY_ID` / `SES_AWS_SECRET_ACCESS_KEY` before falling back to
28
+ generic `AWS_*` credentials, keeping SES account checks separate from
29
+ consumer app S3/ImageCache IAM users.
30
+ - **`ses:verify_domain` existing identity handling** now accepts AWS SES's
31
+ `already exist` response wording and falls back to reading the existing
32
+ identity.
8
33
 
9
34
  ## v0.5.6 (2026-06-14)
10
35
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ Shared Rails engine for McRitchie apps. Provides authentication, error handling,
11
11
  gem "studio-engine", "~> 0.5"
12
12
  ```
13
13
 
14
- Then `bundle install`. The current release is **v0.5.5**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
14
+ Then `bundle install`. The current release is **v0.5.8**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
15
15
 
16
16
  > Published to RubyGems as of v0.4.0 (2026-05-17). New installs should use the RubyGems form, which the consumer Rails apps (`mcritchie-studio`, `turf-monster`) already use.
17
17
 
@@ -35,7 +35,9 @@ Studio.configure do |config|
35
35
  config.auth_methods = %i[magic_link google]
36
36
  config.registration_params = [:name, :email]
37
37
  config.magic_link_token_name = "magic_link_my_app_v1"
38
- config.mailer_from = ENV.fetch("MAILER_FROM", "noreply@example.com")
38
+ config.mailer_from = Studio.mailer_from_for_transport(
39
+ ses_from: "My App <team@example.com>"
40
+ )
39
41
  config.theme_primary = "#4BAF50" # Override default violet
40
42
  config.theme_logos = ["logo.svg"]
41
43
  end
@@ -105,4 +107,7 @@ For short local experiments, temporarily point a consumer Gemfile at `path: "../
105
107
 
106
108
  ## Development Notes
107
109
 
108
- See [CLAUDE.md](./CLAUDE.md) only as legacy migration context while neutral agent docs are being extracted. Current cross-repo setup, ports, credentials, and workflow guidance live in McRitchie Studio's [`docs/agents/`](https://github.com/amcritchie/mcritchie-studio/tree/main/docs/agents).
110
+ Use the docs in [`docs/`](./docs) for engine setup, release, email transport,
111
+ and host-app contracts. Current cross-repo setup, ports, credentials, and
112
+ workflow guidance live in McRitchie Studio's
113
+ [`docs/agents/`](https://github.com/amcritchie/mcritchie-studio/tree/main/docs/agents).
@@ -0,0 +1,35 @@
1
+ module StudioEmailDeliveryHelper
2
+ NON_DELIVERING_EMAIL_METHODS = %w[test file].freeze
3
+
4
+ def email_delivery_banner_status
5
+ delivery_method = studio_email_delivery_method
6
+ capture_enabled = Studio.local_email_capture?
7
+ sends_email = studio_email_perform_deliveries? && !capture_enabled &&
8
+ !NON_DELIVERING_EMAIL_METHODS.include?(delivery_method)
9
+
10
+ "EMAIL SEND #{sends_email} · #{email_delivery_transport_label(delivery_method, capture_enabled)}"
11
+ end
12
+
13
+ def email_delivery_transport_label(delivery_method = studio_email_delivery_method,
14
+ capture_enabled = Studio.local_email_capture?)
15
+ return "capture" if capture_enabled
16
+ return "ses" if Studio.ses_transport_ready?
17
+ return "resend" if delivery_method == "resend"
18
+
19
+ delivery_method.to_s.strip.empty? ? "unknown" : delivery_method
20
+ end
21
+
22
+ private
23
+
24
+ def studio_email_delivery_method
25
+ return "unknown" unless defined?(ActionMailer)
26
+
27
+ ActionMailer::Base.delivery_method.to_s
28
+ end
29
+
30
+ def studio_email_perform_deliveries?
31
+ return false unless defined?(ActionMailer)
32
+
33
+ ActionMailer::Base.perform_deliveries
34
+ end
35
+ end
@@ -3,5 +3,5 @@ class ApplicationMailer < ActionMailer::Base
3
3
  # the host's config/initializers/studio.rb, with an ENV fallback. No layout is
4
4
  # forced — ActionMailer renders bare if the host ships no mailer layout, and a
5
5
  # host that defines its own ApplicationMailer (e.g. turf-monster) wins outright.
6
- default from: -> { Studio.mailer_from || ENV["MAILER_FROM"] || "no-reply@mcritchie.studio" }
6
+ default from: -> { Studio.mailer_from || ENV["MAILER_FROM"] || "McRitchie Studio <team@mcritchie.studio>" }
7
7
  end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.5.6"
2
+ VERSION = "0.5.8"
3
3
  end
data/lib/studio.rb CHANGED
@@ -41,6 +41,7 @@ module Studio
41
41
  # Default From: for engine-sent mail (magic links). Apps set this to their
42
42
  # verified sending address in config/initializers/studio.rb.
43
43
  mattr_accessor :mailer_from, default: nil
44
+ mattr_accessor :resend_mailer_from, default: "McRitchie Studio <team@mcritchie.studio>"
44
45
 
45
46
  # Local/worktree email capture. nil means "auto": enabled when AGENT_WORKTREE
46
47
  # is truthy, otherwise disabled. Production always disables capture.
@@ -88,6 +89,36 @@ module Studio
88
89
  yield self
89
90
  end
90
91
 
92
+ def self.mailer_from_for_transport(env: ENV, ses_from:, resend_from: nil)
93
+ if ses_transport_ready?(env)
94
+ env_value(env, "MAILER_FROM") || ses_from
95
+ else
96
+ env_value(env, "RESEND_MAILER_FROM") || resend_from || resend_mailer_from
97
+ end
98
+ end
99
+
100
+ def self.marketing_from_for_transport(env: ENV, ses_from:, resend_from: nil)
101
+ if ses_transport_ready?(env)
102
+ env_value(env, "MARKETING_MAILER_FROM") || ses_from
103
+ else
104
+ env_value(env, "RESEND_MARKETING_FROM") ||
105
+ env_value(env, "RESEND_MAILER_FROM") ||
106
+ resend_from ||
107
+ resend_mailer_from
108
+ end
109
+ end
110
+
111
+ def self.ses_transport_ready?(env = ENV)
112
+ env["MAIL_TRANSPORT"].to_s.downcase == "ses" &&
113
+ env_value(env, "SES_SMTP_USERNAME") &&
114
+ env_value(env, "SES_SMTP_PASSWORD")
115
+ end
116
+
117
+ def self.env_value(env, key)
118
+ value = env[key]
119
+ value if value && !value.to_s.strip.empty?
120
+ end
121
+
91
122
  # True when the given sign-in method is enabled for this app.
92
123
  def self.auth_method?(method)
93
124
  auth_methods.include?(method.to_sym)
@@ -2,14 +2,39 @@
2
2
 
3
3
  unless Rake::Task.task_defined?("ses:check")
4
4
  namespace :ses do
5
+ def studio_ses_env_value(name)
6
+ value = ENV[name]
7
+ return nil if value.nil? || value.to_s.strip.empty?
8
+
9
+ value
10
+ end
11
+
12
+ def studio_ses_credential(name)
13
+ studio_ses_env_value("SES_#{name}") || studio_ses_env_value(name)
14
+ end
15
+
16
+ def studio_ses_credential_source(name)
17
+ return "SES_#{name}" if studio_ses_env_value("SES_#{name}")
18
+ return name if studio_ses_env_value(name)
19
+
20
+ "missing"
21
+ end
22
+
5
23
  def studio_ses_signer(region)
6
24
  require "aws-sigv4"
7
25
 
26
+ access_key_id = studio_ses_credential("AWS_ACCESS_KEY_ID")
27
+ secret_access_key = studio_ses_credential("AWS_SECRET_ACCESS_KEY")
28
+ missing = []
29
+ missing << "SES_AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_ID" unless access_key_id
30
+ missing << "SES_AWS_SECRET_ACCESS_KEY or AWS_SECRET_ACCESS_KEY" unless secret_access_key
31
+ abort "ses:* needs #{missing.join(' and ')}." if missing.any?
32
+
8
33
  Aws::Sigv4::Signer.new(
9
34
  service: "ses",
10
35
  region: region,
11
- access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"),
12
- secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY")
36
+ access_key_id: access_key_id,
37
+ secret_access_key: secret_access_key
13
38
  )
14
39
  rescue LoadError
15
40
  abort "ses:* needs aws-sigv4; it is usually available through aws-sdk-s3."
@@ -45,6 +70,7 @@ unless Rake::Task.task_defined?("ses:check")
45
70
 
46
71
  code, account = get.call("/v2/email/account")
47
72
  puts "GetAccount (region #{region}) -> HTTP #{code}"
73
+ puts " CredentialSource=#{studio_ses_credential_source('AWS_ACCESS_KEY_ID')}"
48
74
  if code == 200
49
75
  puts " SendingEnabled=#{account['SendingEnabled']} ProductionAccessEnabled=#{account['ProductionAccessEnabled']} Enforcement=#{account['EnforcementStatus']}"
50
76
  else
@@ -74,8 +100,9 @@ unless Rake::Task.task_defined?("ses:check")
74
100
  signer = studio_ses_signer(region)
75
101
  body = { EmailIdentity: domain }.to_json
76
102
  code, response = studio_ses_request(signer, region, "POST", "/v2/email/identities", body)
103
+ already_exists = "#{response['message']} #{response['Message']}".match?(/already exist/i)
77
104
 
78
- if code == 409 || "#{response['message']}#{response['Message']}".match?(/already exists/i)
105
+ if code == 409 || ([400, 409].include?(code) && already_exists)
79
106
  code, response = studio_ses_request(signer, region, "GET", "/v2/email/identities/#{domain}")
80
107
  end
81
108
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.6
4
+ version: 0.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -151,6 +152,7 @@ files:
151
152
  - app/controllers/solana_sessions_controller.rb
152
153
  - app/controllers/studio/local_emails_controller.rb
153
154
  - app/controllers/theme_settings_controller.rb
155
+ - app/helpers/studio_email_delivery_helper.rb
154
156
  - app/helpers/studio_theme_helper.rb
155
157
  - app/jobs/error_log_cleanup_job.rb
156
158
  - app/jobs/studio/email_delivery_job.rb
@@ -226,6 +228,7 @@ metadata:
226
228
  source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
227
229
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
228
230
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
231
+ post_install_message:
229
232
  rdoc_options: []
230
233
  require_paths:
231
234
  - lib
@@ -240,7 +243,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
240
243
  - !ruby/object:Gem::Version
241
244
  version: '0'
242
245
  requirements: []
243
- rubygems_version: 4.0.9
246
+ rubygems_version: 3.5.11
247
+ signing_key:
244
248
  specification_version: 4
245
249
  summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
246
250
  image caching