studio-engine 0.5.0 → 0.5.2

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: '01761814c3988b2d5748c44b65122bb83678455e1ae1ee7f5daa1b949c1c0d65'
4
- data.tar.gz: 42a25a49ff120d404827d22298440f1c0cb0b981c8a7c549aaf15e218fa666a8
3
+ metadata.gz: f351e5f2cf75e05dfc8befdd34cb525310ab4c4415bb084d15c57a1e3713b782
4
+ data.tar.gz: f84aad6087299527b4cdb330e644ee707f695b6a70597c753039c8b7af81ee93
5
5
  SHA512:
6
- metadata.gz: 44831601ac48a7dfcfb68dcf0173e7c10aa5be021935fed8c609a44aca3736393b4e556dbcf45ae9a1c4bb4691682caaedb21c7e5316859c720e164189a886da
7
- data.tar.gz: 3bbc9bcf51bc62253d7e034f64f6e26e80222e8ae5cdaeef148efcd0e99967ddabcaaf002a80a29ac941f9e04185621c74a1969a3822071db68f70e0a1970cb2
6
+ metadata.gz: fc184764831f825771d76f9cc93ae51f0103823c571ad886431beeffeb1ba55c15907004dc1796d6901fdafc560571c02cbde8fdd02141f2629c5cd60071f5a0
7
+ data.tar.gz: 20a76451338e9aea84191a1c5f3efb995c46514117e18e80d014bda75524ff50e6288abb88da26b84ce463fd296f601e63580de9bfb729458c31cfaf54fa96dc
data/CHANGELOG.md CHANGED
@@ -1,6 +1,32 @@
1
1
  # Changelog
2
2
 
3
- The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Both consumer Rails apps pin to a tag in their `Gemfile`; bumping the tag is a release.
3
+ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.5"`; bumping the gem version and updating consumer lockfiles is a release.
4
+
5
+ ## Unreleased
6
+
7
+ No entries yet.
8
+
9
+ ## v0.5.2 (2026-06-13)
10
+
11
+ ### Added
12
+ - **`Studio::MailTransport`** — shared ActionMailer transport selection for SES SMTP primary and Resend rollback.
13
+ - **`ses:check` / `ses:verify_domain`** — shared Rake tasks for SES credential and domain verification checks.
14
+ - **`bin/release-check`** — local preflight for Ruby syntax, engine unit tests, and optional gem packaging.
15
+
16
+ ### Changed
17
+ - Engine release docs now describe the RubyGems flow and consumer lockfile adoption instead of the legacy git tag pinning flow.
18
+ - Runtime dependency ranges are now bounded for cleaner RubyGems releases while preserving the current Rails 7 / Solid Queue 1.x app stack.
19
+ - `resend` is declared as an engine runtime dependency so consumer apps can drop direct rollback dependencies after they bundle the release that includes `Studio::MailTransport`.
20
+
21
+ ## v0.5.1 (2026-06-02)
22
+
23
+ Smooths the turf-monster adoption of the v0.5.0 auth core (turf already ships its own battle-tested auth routes).
24
+
25
+ ### Added
26
+ - **`Studio.draw_auth_routes`** (default `true`) — gates the `magic_link` + `solana` route block in `Studio.routes`. An app that already defines those routes (turf-monster) sets it `false` to keep its own routes and avoid a duplicate route-NAME boot crash.
27
+
28
+ ### Changed
29
+ - **`MagicLink`** re-exposes `TOKEN_KEY` + `TTL` constants (equal to the config defaults) for back-compat with consumer code/tests that reference them; behavior is still driven by `Studio.magic_link_token_name` / `Studio.magic_link_ttl`.
4
30
 
5
31
  ## v0.5.0 (2026-06-02)
6
32
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Studio Engine
2
2
 
3
- Shared Rails engine for McRitchie apps. Provides authentication, error handling, dynamic theming, and common concerns used by [McRitchie Studio](https://app.mcritchie.studio) and [Turf Monster](https://turf.mcritchie.studio).
3
+ Shared Rails engine for McRitchie apps. Provides authentication, error handling, dynamic theming, and common concerns used by [McRitchie Studio](https://app.mcritchie.studio) and [Turf Monster](https://app.turfmonster.media).
4
4
 
5
5
  > **Part of the McRitchie ecosystem** — see [`ECOSYSTEM.md`](https://github.com/amcritchie/mcritchie-studio/blob/main/docs/ECOSYSTEM.md) for the 5-repo map; [`house-burn-down.md`](https://github.com/amcritchie/mcritchie-studio/blob/main/docs/agents/system/house-burn-down.md) for fresh-Mac recovery.
6
6
 
@@ -8,16 +8,16 @@ Shared Rails engine for McRitchie apps. Provides authentication, error handling,
8
8
 
9
9
  ```ruby
10
10
  # Gemfile — install from RubyGems (recommended)
11
- gem "studio-engine", "~> 0.4.0"
11
+ gem "studio-engine", "~> 0.5"
12
12
  ```
13
13
 
14
- Then `bundle install`. The current release is **v0.4.10**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
14
+ Then `bundle install`. The current release is **v0.5.2**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
15
15
 
16
- > Published to RubyGems as of v0.4.0 (2026-05-17). Earlier consumers used a `git:` ref pinned to a tag; that pattern is preserved here for reference but new installs should use the RubyGems form, which the consumer Rails apps (`mcritchie-studio`, `turf-monster`, `tax-studio`) already use.
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
 
18
18
  ## What It Provides
19
19
 
20
- - **Authentication**: Session-based login/signup controllers and views, Google OAuth via OmniAuth, one-way SSO (hub to satellite)
20
+ - **Authentication**: Passwordless magic-link auth, optional password auth, Google OAuth via OmniAuth, Solana wallet sign-in, and optional one-way SSO patterns
21
21
  - **Error handling**: `Studio::ErrorHandling` concern with `rescue_and_log`, `ErrorLog` model with `capture!`, error log viewer at `/error_logs`
22
22
  - **Theme system**: Dynamic CSS custom properties generated from 7 role colors (primary, dark, light, success, accent, warning, danger). Dark/light mode toggle. Admin theme editor at `/admin/theme`.
23
23
  - **Sluggable concern**: `before_save :set_slug` with `to_param` for human-readable URLs
@@ -31,14 +31,26 @@ Each consuming app configures the engine in `config/initializers/studio.rb`:
31
31
  Studio.configure do |config|
32
32
  config.app_name = "My App"
33
33
  config.session_key = :my_app_user_id
34
- config.sso_logo = "/logo.svg"
35
34
  config.welcome_message = ->(user) { "Welcome, #{user.display_name}!" }
36
- config.registration_params = [:name, :email, :password, :password_confirmation]
35
+ config.auth_methods = %i[magic_link google]
36
+ config.registration_params = [:name, :email]
37
+ config.magic_link_token_name = "magic_link_my_app_v1"
38
+ config.mailer_from = ENV.fetch("MAILER_FROM", "noreply@example.com")
37
39
  config.theme_primary = "#4BAF50" # Override default violet
38
40
  config.theme_logos = ["logo.svg"]
39
41
  end
40
42
  ```
41
43
 
44
+ Transactional mail transport is shared through `Studio::MailTransport`:
45
+
46
+ ```ruby
47
+ # config/initializers/studio_mail_transport.rb
48
+ Studio::MailTransport.configure!
49
+ ```
50
+
51
+ It selects SES SMTP when `MAIL_TRANSPORT=ses` and SES SMTP credentials are
52
+ present, otherwise falls back to Resend when `RESEND_API_KEY` is present.
53
+
42
54
  ## Routes
43
55
 
44
56
  In the consuming app's `config/routes.rb`:
@@ -50,7 +62,7 @@ Rails.application.routes.draw do
50
62
  end
51
63
  ```
52
64
 
53
- This draws: `/login`, `/signup`, `/logout`, `/sso_continue`, `/sso_login`, `/auth/:provider/callback`, `/auth/failure`, `/error_logs`, `/admin/theme` (GET, PATCH), `/admin/theme/regenerate`.
65
+ This draws the enabled auth routes (`/login`, `/signup`, `/logout`, magic-link routes, Solana routes), OAuth callbacks, optional SSO routes, `/error_logs`, and `/admin/theme`.
54
66
 
55
67
  ## Overriding Views
56
68
 
@@ -58,19 +70,20 @@ This is a non-isolated engine -- app views at the same path automatically overri
58
70
 
59
71
  ## Releasing
60
72
 
61
- Engine releases are git tags (semver: `MAJOR.MINOR.PATCH`). Both consumer apps pin to a tag in their Gemfile — bumping the tag is the release.
73
+ Engine releases use semantic versions and are published to RubyGems. The full
74
+ operator checklist lives in [`docs/RELEASE.md`](./docs/RELEASE.md).
75
+
76
+ Short form:
62
77
 
63
- 1. Make + commit changes on `main`.
64
- 2. Update [`CHANGELOG.md`](./CHANGELOG.md) with the new version + a `### Added` / `### Changed` / `### Removed` summary. Keep entries terse.
65
- 3. Bump `lib/studio/version.rb` to match.
66
- 4. Commit the version bump + CHANGELOG together (`v0.X.Y: <summary>`).
67
- 5. Tag: `git tag -a v0.X.Y -m "<one-line summary>"`.
68
- 6. Push: `git push origin main --tags`.
69
- 7. In each consumer app's Gemfile, update the `tag:` field. Commit + push.
70
- 8. On consumer prod: `bundle install` runs as part of the deploy buildpack.
78
+ 1. Update [`CHANGELOG.md`](./CHANGELOG.md) and `lib/studio/version.rb`.
79
+ 2. Run `bin/release-check --build`.
80
+ 3. Publish the gem only after explicit approval.
81
+ 4. Tag the release after RubyGems accepts the gem.
82
+ 5. In each consumer app, run `bundle update studio-engine`, verify the lockfile,
83
+ and run app smoke checks.
71
84
 
72
85
  **Semver guide**
73
- - **PATCH**: bug fix; no API change. Consumers can bump tag with zero diff elsewhere.
86
+ - **PATCH**: bug fix; no API change. Consumers can update the gem with zero diff elsewhere.
74
87
  - **MINOR**: backward-compatible feature add. Consumers may opt in to new APIs.
75
88
  - **MAJOR**: breaking change. Consumers will need code changes alongside the tag bump.
76
89
 
@@ -83,11 +96,11 @@ When iterating on engine code from a consumer app, point bundler at the local pa
83
96
  bundle config set --local local.studio /Users/alex/projects/studio-engine
84
97
  bundle install
85
98
  # ... iterate in both repos ...
86
- bundle config unset --local local.studio # restore tag-pinned resolution
99
+ bundle config unset --local local.studio # restore RubyGems resolution
87
100
  ```
88
101
 
89
- Note: `bundle config local.studio` requires `branch:` in the Gemfile entry. If you frequently develop locally, change the Gemfile to `gem "studio-engine", git: "...", branch: "main"` during dev and back to `tag:` before merging.
102
+ For short local experiments, temporarily point a consumer Gemfile at `path: "../studio-engine"` and restore the RubyGems dependency before merging.
90
103
 
91
104
  ## Development Notes
92
105
 
93
- See [CLAUDE.md](./CLAUDE.md) for detailed development context including the theme architecture, SSO protocol, color scale system, and code conventions.
106
+ 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).
@@ -21,6 +21,13 @@
21
21
  #
22
22
  # Lifted into studio-engine (was turf-monster app/services/magic_link.rb).
23
23
  class MagicLink
24
+ # Back-compat defaults. Behavior is driven by the `token_name` / `ttl` methods
25
+ # (which read Studio config); these constants remain so existing consumer code
26
+ # /tests referencing MagicLink::TTL keep working, and they equal the config
27
+ # defaults.
28
+ TOKEN_KEY = "magic_link_v1"
29
+ TTL = 15.minutes
30
+
24
31
  class InvalidToken < StandardError; end
25
32
 
26
33
  Result = Struct.new(:email, :return_to, keyword_init: true)
data/lib/studio/engine.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  module Studio
2
2
  class Engine < ::Rails::Engine
3
+ rake_tasks do
4
+ load File.expand_path("../tasks/studio_ses.rake", __dir__)
5
+ end
6
+
3
7
  config.after_initialize do
4
8
  # Validate the host app's User model satisfies the engine's contract.
5
9
  # See docs/USER_CONTRACT.md. Opt out with Studio.validate_user_contract = false.
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Studio
4
+ class MailTransport
5
+ Result = Struct.new(:transport, :delivery_method, :message, keyword_init: true)
6
+
7
+ class << self
8
+ def configure!(env: ENV,
9
+ rails_env: defined?(Rails) ? Rails.env : "development",
10
+ action_mailer: defined?(ActionMailer) ? ActionMailer::Base : nil,
11
+ logger: defined?(Rails) ? Rails.logger : nil,
12
+ mailer_from: defined?(Studio) && Studio.respond_to?(:mailer_from) ? Studio.mailer_from : nil,
13
+ resend_loader: method(:load_resend!),
14
+ resend_configurer: method(:configure_resend!))
15
+ raise ArgumentError, "action_mailer is required" unless action_mailer
16
+
17
+ if rails_env.to_s == "test"
18
+ return Result.new(transport: :test, delivery_method: action_mailer.delivery_method, message: "test environment skipped")
19
+ end
20
+
21
+ selected = env["MAIL_TRANSPORT"].to_s.downcase
22
+ ses_ready = selected == "ses" && present?(env["SES_SMTP_USERNAME"]) && present?(env["SES_SMTP_PASSWORD"])
23
+
24
+ if selected == "ses" && !ses_ready
25
+ log(logger, :warn, "[mail] MAIL_TRANSPORT=ses but SES_SMTP_USERNAME/PASSWORD missing - keeping fallback transport")
26
+ elsif present?(selected) && !%w[ses resend].include?(selected)
27
+ log(logger, :warn, "[mail] unknown MAIL_TRANSPORT=#{selected.inspect} - keeping fallback transport")
28
+ end
29
+
30
+ if ses_ready
31
+ configure_ses!(env: env, action_mailer: action_mailer)
32
+ region = env.fetch("SES_REGION", "us-east-2")
33
+ log(logger, :info, "[mail] transport=SES region=#{region} from=#{mailer_from}")
34
+ return Result.new(transport: :ses, delivery_method: action_mailer.delivery_method, message: "SES SMTP active")
35
+ end
36
+
37
+ if present?(env["RESEND_API_KEY"])
38
+ resend_loader.call
39
+ resend_configurer.call(env["RESEND_API_KEY"])
40
+ action_mailer.delivery_method = :resend
41
+ log(logger, :info, "[mail] transport=Resend from=#{mailer_from}")
42
+ return Result.new(transport: :resend, delivery_method: action_mailer.delivery_method, message: "Resend active")
43
+ end
44
+
45
+ Result.new(transport: :default, delivery_method: action_mailer.delivery_method, message: "no transactional transport configured")
46
+ end
47
+
48
+ def configure_ses!(env:, action_mailer:)
49
+ region = env.fetch("SES_REGION", "us-east-2")
50
+ action_mailer.delivery_method = :smtp
51
+ action_mailer.smtp_settings = {
52
+ address: env.fetch("SES_SMTP_HOST", "email-smtp.#{region}.amazonaws.com"),
53
+ port: env.fetch("SES_SMTP_PORT", 587).to_i,
54
+ user_name: env["SES_SMTP_USERNAME"],
55
+ password: env["SES_SMTP_PASSWORD"],
56
+ authentication: :login,
57
+ enable_starttls_auto: true
58
+ }
59
+ end
60
+
61
+ def load_resend!
62
+ require "resend"
63
+ end
64
+
65
+ def configure_resend!(api_key)
66
+ Resend.api_key = api_key
67
+ end
68
+
69
+ private
70
+
71
+ def present?(value)
72
+ !value.to_s.strip.empty?
73
+ end
74
+
75
+ def log(logger, level, message)
76
+ return unless logger
77
+
78
+ logger.public_send(level, message)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.2"
3
3
  end
data/lib/studio.rb CHANGED
@@ -5,6 +5,7 @@ require "studio/theme_resolver"
5
5
  require "studio/username_generator"
6
6
  require "studio/s3"
7
7
  require "studio/image_cache"
8
+ require "studio/mail_transport"
8
9
 
9
10
  module Studio
10
11
  mattr_accessor :app_name, default: "Studio"
@@ -29,6 +30,12 @@ module Studio
29
30
  mattr_accessor :magic_link_ttl, default: 15.minutes
30
31
  mattr_accessor :magic_link_token_name, default: "magic_link_v1"
31
32
 
33
+ # Whether Studio.routes draws the magic_link + solana wallet routes. An app that
34
+ # already defines its own auth routes (e.g. turf-monster, which has battle-tested
35
+ # magic_link/solana routes + extras) sets this false to avoid duplicate route
36
+ # NAMES at boot, keeping its own routes intact. New consumers leave it true.
37
+ mattr_accessor :draw_auth_routes, default: true
38
+
32
39
  # Default From: for engine-sent mail (magic links). Apps set this to their
33
40
  # verified Resend sending address in config/initializers/studio.rb.
34
41
  mattr_accessor :mailer_from, default: nil
@@ -154,7 +161,7 @@ module Studio
154
161
  # to request a link) + magic_link_path(token) / magic_link_url(token:)
155
162
  # (the emailed consume link). The token is a URL-safe MessageVerifier blob
156
163
  # but the constraint guards against a stray "." segment.
157
- if Studio.auth_method?(:magic_link)
164
+ if Studio.draw_auth_routes && Studio.auth_method?(:magic_link)
158
165
  post "magic_link", to: "magic_links#create", as: :magic_link_request
159
166
  get "magic_link/:token", to: "magic_links#consume", as: :magic_link,
160
167
  constraints: { token: %r{[^/]+} }
@@ -164,7 +171,7 @@ module Studio
164
171
  # The browser posts to these literal paths from the shared Connect-Wallet
165
172
  # flow; app-specific surfaces (mobile deep-link callback, account-linking,
166
173
  # OAuth popup) stay in the consuming app's routes.
167
- if Studio.auth_method?(:wallet)
174
+ if Studio.draw_auth_routes && Studio.auth_method?(:wallet)
168
175
  get "auth/solana/nonce", to: "solana_sessions#nonce", as: :solana_nonce
169
176
  post "auth/solana/verify", to: "solana_sessions#verify", as: :solana_verify
170
177
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless Rake::Task.task_defined?("ses:check")
4
+ namespace :ses do
5
+ def studio_ses_signer(region)
6
+ require "aws-sigv4"
7
+
8
+ Aws::Sigv4::Signer.new(
9
+ service: "ses",
10
+ region: region,
11
+ access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"),
12
+ secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY")
13
+ )
14
+ rescue LoadError
15
+ abort "ses:* needs aws-sigv4; it is usually available through aws-sdk-s3."
16
+ end
17
+
18
+ def studio_ses_request(signer, region, method, path, body = nil)
19
+ require "net/http"
20
+ require "json"
21
+
22
+ url = "https://email.#{region}.amazonaws.com#{path}"
23
+ headers = body ? { "content-type" => "application/json" } : {}
24
+ sig = signer.sign_request(http_method: method, url: url, body: body, headers: headers)
25
+ uri = URI(url)
26
+ http = Net::HTTP.new(uri.host, 443)
27
+ http.use_ssl = true
28
+ req = Net::HTTP.const_get(method.capitalize).new(uri)
29
+ headers.each { |key, value| req[key] = value }
30
+ sig.headers.each { |key, value| req[key] = value }
31
+ req.body = body if body
32
+ res = http.request(req)
33
+ [res.code.to_i, (JSON.parse(res.body) rescue { "raw" => res.body.to_s[0, 300] })]
34
+ end
35
+
36
+ def studio_ses_error(body)
37
+ body["message"] || body["Message"] || body["raw"]
38
+ end
39
+
40
+ desc "Check SES account status and verified identities"
41
+ task check: :environment do
42
+ region = ENV.fetch("SES_REGION", "us-east-2")
43
+ signer = studio_ses_signer(region)
44
+ get = ->(path) { studio_ses_request(signer, region, "GET", path) }
45
+
46
+ code, account = get.call("/v2/email/account")
47
+ puts "GetAccount (region #{region}) -> HTTP #{code}"
48
+ if code == 200
49
+ puts " SendingEnabled=#{account['SendingEnabled']} ProductionAccessEnabled=#{account['ProductionAccessEnabled']} Enforcement=#{account['EnforcementStatus']}"
50
+ else
51
+ puts " ERROR: #{studio_ses_error(account)}"
52
+ end
53
+
54
+ code, identities = get.call("/v2/email/identities")
55
+ puts "ListEmailIdentities -> HTTP #{code}"
56
+ if code == 200
57
+ list = identities["EmailIdentities"] || []
58
+ names = list.map { |identity| "#{identity['IdentityName']}(#{identity['VerifiedForSendingStatus'] ? 'verified' : 'pending'})" }
59
+ puts " identities: #{names.empty? ? '(none yet)' : names.join(', ')}"
60
+ else
61
+ puts " ERROR: #{studio_ses_error(identities)}"
62
+ end
63
+
64
+ transport = ENV.fetch("MAIL_TRANSPORT", "(unset -> resend)")
65
+ puts "Live transport: MAIL_TRANSPORT=#{transport} delivery_method=#{ActionMailer::Base.delivery_method}"
66
+ end
67
+
68
+ desc "Create a SES domain identity and print DKIM CNAME records"
69
+ task :verify_domain, [:domain] => :environment do |_task, args|
70
+ abort "Usage: ses:verify_domain[domain]" if args[:domain].blank?
71
+
72
+ domain = args[:domain]
73
+ region = ENV.fetch("SES_REGION", "us-east-2")
74
+ signer = studio_ses_signer(region)
75
+ body = { EmailIdentity: domain }.to_json
76
+ code, response = studio_ses_request(signer, region, "POST", "/v2/email/identities", body)
77
+
78
+ if code == 409 || "#{response['message']}#{response['Message']}".match?(/already exists/i)
79
+ code, response = studio_ses_request(signer, region, "GET", "/v2/email/identities/#{domain}")
80
+ end
81
+
82
+ if code != 200
83
+ puts "#{domain}: ERROR (HTTP #{code}) #{studio_ses_error(response)}"
84
+ next
85
+ end
86
+
87
+ tokens = response.dig("DkimAttributes", "Tokens") || []
88
+ status = response.dig("DkimAttributes", "Status") || response["VerifiedForSendingStatus"]
89
+ puts "== #{domain} (region #{region}, DKIM status: #{status}) =="
90
+ if tokens.empty?
91
+ puts " (no DKIM tokens returned)"
92
+ else
93
+ puts " Add these 3 CNAME records to #{domain}'s DNS:"
94
+ tokens.each do |token|
95
+ puts " NAME: #{token}._domainkey.#{domain}"
96
+ puts " TYPE: CNAME"
97
+ puts " VALUE: #{token}.dkim.amazonses.com"
98
+ puts ""
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
 
14
14
  spec.metadata = {
15
15
  "homepage_uri" => "https://github.com/amcritchie/studio-engine",
16
- "source_code_uri" => "https://github.com/amcritchie/studio-engine",
16
+ "source_code_uri" => "https://github.com/amcritchie/studio-engine/tree/main",
17
17
  "bug_tracker_uri" => "https://github.com/amcritchie/studio-engine/issues",
18
18
  "changelog_uri" => "https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md"
19
19
  }
@@ -21,10 +21,11 @@ Gem::Specification.new do |spec|
21
21
  spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "tailwind/**/*", "Gemfile", "studio-engine.gemspec", "README.md", "CHANGELOG.md", "LICENSE"]
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.add_dependency "rails", ">= 7.0"
24
+ spec.add_dependency "rails", ">= 7.0", "< 8.0"
25
25
  spec.add_dependency "tailwindcss-rails", "~> 2.7"
26
- spec.add_dependency "faker", ">= 2.0"
27
- spec.add_dependency "solid_queue"
26
+ spec.add_dependency "faker", ">= 2.0", "< 4.0"
27
+ spec.add_dependency "solid_queue", ">= 1.0", "< 2.0"
28
28
  spec.add_dependency "aws-sdk-s3", "~> 1.218"
29
29
  spec.add_dependency "mini_magick", "~> 5.0"
30
+ spec.add_dependency "resend", "~> 1.1"
30
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-02 00:00:00.000000000 Z
11
+ date: 2026-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '7.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,9 @@ dependencies:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '7.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: tailwindcss-rails
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -45,6 +51,9 @@ dependencies:
45
51
  - - ">="
46
52
  - !ruby/object:Gem::Version
47
53
  version: '2.0'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '4.0'
48
57
  type: :runtime
49
58
  prerelease: false
50
59
  version_requirements: !ruby/object:Gem::Requirement
@@ -52,20 +61,29 @@ dependencies:
52
61
  - - ">="
53
62
  - !ruby/object:Gem::Version
54
63
  version: '2.0'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '4.0'
55
67
  - !ruby/object:Gem::Dependency
56
68
  name: solid_queue
57
69
  requirement: !ruby/object:Gem::Requirement
58
70
  requirements:
59
71
  - - ">="
60
72
  - !ruby/object:Gem::Version
61
- version: '0'
73
+ version: '1.0'
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '2.0'
62
77
  type: :runtime
63
78
  prerelease: false
64
79
  version_requirements: !ruby/object:Gem::Requirement
65
80
  requirements:
66
81
  - - ">="
67
82
  - !ruby/object:Gem::Version
68
- version: '0'
83
+ version: '1.0'
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '2.0'
69
87
  - !ruby/object:Gem::Dependency
70
88
  name: aws-sdk-s3
71
89
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +112,20 @@ dependencies:
94
112
  - - "~>"
95
113
  - !ruby/object:Gem::Version
96
114
  version: '5.0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: resend
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1.1'
122
+ type: :runtime
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1.1'
97
129
  description: Studio Engine is a non-isolated Rails engine that ships an opinionated
98
130
  authentication + SSO contract, a polymorphic ErrorLog model, a Sluggable concern,
99
131
  a 7-role dynamic theme system with CSS-custom-property generation, and an S3-backed
@@ -172,10 +204,12 @@ files:
172
204
  - lib/studio/color_scale.rb
173
205
  - lib/studio/engine.rb
174
206
  - lib/studio/image_cache.rb
207
+ - lib/studio/mail_transport.rb
175
208
  - lib/studio/s3.rb
176
209
  - lib/studio/theme_resolver.rb
177
210
  - lib/studio/username_generator.rb
178
211
  - lib/studio/version.rb
212
+ - lib/tasks/studio_ses.rake
179
213
  - studio-engine.gemspec
180
214
  - tailwind/studio.tailwind.config.js
181
215
  homepage: https://github.com/amcritchie/studio-engine
@@ -183,7 +217,7 @@ licenses:
183
217
  - MIT
184
218
  metadata:
185
219
  homepage_uri: https://github.com/amcritchie/studio-engine
186
- source_code_uri: https://github.com/amcritchie/studio-engine
220
+ source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
187
221
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
188
222
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
189
223
  post_install_message: