source_monitor 0.13.1 → 0.14.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +1 -0
  3. data/.claude/skills/sm-configure/SKILL.md +8 -1
  4. data/.claude/skills/sm-configure/reference/configuration-reference.md +11 -0
  5. data/.claude/skills/sm-host-setup/SKILL.md +13 -3
  6. data/.claude/skills/sm-host-setup/reference/initializer-template.md +11 -0
  7. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +9 -1
  8. data/.claude/skills/sm-upgrade/reference/version-history.md +12 -0
  9. data/CHANGELOG.md +15 -0
  10. data/Gemfile.lock +1 -1
  11. data/README.md +3 -3
  12. data/VERSION +1 -1
  13. data/app/controllers/source_monitor/application_controller.rb +73 -14
  14. data/app/views/layouts/source_monitor/application.html.erb +6 -0
  15. data/docs/configuration.md +18 -1
  16. data/docs/deployment.md +1 -1
  17. data/docs/goals/engine-hardening/.goalbuddy-board/app.js +543 -0
  18. data/docs/goals/engine-hardening/.goalbuddy-board/goalbuddy-mark.png +0 -0
  19. data/docs/goals/engine-hardening/.goalbuddy-board/index.html +111 -0
  20. data/docs/goals/engine-hardening/.goalbuddy-board/styles.css +991 -0
  21. data/docs/goals/engine-hardening/goal.md +97 -0
  22. data/docs/goals/engine-hardening/notes/T001-spec-validation.md +37 -0
  23. data/docs/goals/engine-hardening/state.yaml +324 -0
  24. data/docs/setup.md +3 -3
  25. data/docs/upgrade.md +27 -0
  26. data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +10 -0
  27. data/lib/source_monitor/configuration/authentication_settings.rb +5 -1
  28. data/lib/source_monitor/security/authentication.rb +10 -0
  29. data/lib/source_monitor/version.rb +1 -1
  30. data/source_monitor.gemspec +7 -2
  31. metadata +8 -65
  32. data/.claude/agent-memory/vbw-vbw-debugger/MEMORY.md +0 -15
  33. data/.claude/agent-memory/vbw-vbw-dev/MEMORY.md +0 -34
  34. data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +0 -49
  35. data/.claude/agents/rails-concern.md +0 -464
  36. data/.claude/agents/rails-controller.md +0 -424
  37. data/.claude/agents/rails-hotwire.md +0 -446
  38. data/.claude/agents/rails-implement.md +0 -374
  39. data/.claude/agents/rails-job.md +0 -334
  40. data/.claude/agents/rails-lint.md +0 -294
  41. data/.claude/agents/rails-mailer.md +0 -371
  42. data/.claude/agents/rails-migration.md +0 -449
  43. data/.claude/agents/rails-model.md +0 -420
  44. data/.claude/agents/rails-policy.md +0 -443
  45. data/.claude/agents/rails-presenter.md +0 -427
  46. data/.claude/agents/rails-query.md +0 -412
  47. data/.claude/agents/rails-review.md +0 -490
  48. data/.claude/agents/rails-service.md +0 -458
  49. data/.claude/agents/rails-state-records.md +0 -465
  50. data/.claude/agents/rails-tdd.md +0 -314
  51. data/.claude/agents/rails-test.md +0 -441
  52. data/.claude/agents/rails-view-component.md +0 -418
  53. data/.claude/commands/rails-audit.md +0 -77
  54. data/.claude/commands/release.md +0 -366
  55. data/.claude/hooks/block-secrets.sh +0 -52
  56. data/.claude/settings.json +0 -85
  57. data/.claude/skills/action-cable-patterns/SKILL.md +0 -296
  58. data/.claude/skills/action-mailer-patterns/SKILL.md +0 -295
  59. data/.claude/skills/active-storage-setup/SKILL.md +0 -311
  60. data/.claude/skills/api-versioning/SKILL.md +0 -294
  61. data/.claude/skills/authentication-flow/SKILL.md +0 -335
  62. data/.claude/skills/authentication-flow/reference/current.md +0 -248
  63. data/.claude/skills/authentication-flow/reference/passwordless.md +0 -253
  64. data/.claude/skills/authentication-flow/reference/sessions.md +0 -201
  65. data/.claude/skills/authorization-pundit/SKILL.md +0 -462
  66. data/.claude/skills/caching-strategies/SKILL.md +0 -350
  67. data/.claude/skills/database-migrations/SKILL.md +0 -354
  68. data/.claude/skills/form-object-patterns/SKILL.md +0 -399
  69. data/.claude/skills/hotwire-patterns/SKILL.md +0 -247
  70. data/.claude/skills/hotwire-patterns/reference/stimulus.md +0 -307
  71. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +0 -112
  72. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +0 -158
  73. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +0 -218
  74. data/.claude/skills/i18n-patterns/SKILL.md +0 -320
  75. data/.claude/skills/install/SKILL.md +0 -367
  76. data/.claude/skills/performance-optimization/SKILL.md +0 -311
  77. data/.claude/skills/rails-architecture/SKILL.md +0 -259
  78. data/.claude/skills/rails-architecture/reference/error-handling.md +0 -333
  79. data/.claude/skills/rails-architecture/reference/event-tracking.md +0 -142
  80. data/.claude/skills/rails-architecture/reference/layer-interactions.md +0 -417
  81. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +0 -152
  82. data/.claude/skills/rails-architecture/reference/query-patterns.md +0 -342
  83. data/.claude/skills/rails-architecture/reference/service-patterns.md +0 -286
  84. data/.claude/skills/rails-architecture/reference/state-records.md +0 -250
  85. data/.claude/skills/rails-architecture/reference/testing-strategy.md +0 -326
  86. data/.claude/skills/rails-concern/SKILL.md +0 -399
  87. data/.claude/skills/rails-controller/SKILL.md +0 -336
  88. data/.claude/skills/rails-model-generator/SKILL.md +0 -321
  89. data/.claude/skills/rails-model-generator/reference/validations.md +0 -298
  90. data/.claude/skills/rails-presenter/SKILL.md +0 -274
  91. data/.claude/skills/rails-query-object/SKILL.md +0 -289
  92. data/.claude/skills/rails-service-object/SKILL.md +0 -349
  93. data/.claude/skills/solid-queue-setup/SKILL.md +0 -307
  94. data/.claude/skills/tdd-cycle/SKILL.md +0 -359
  95. data/.claude/skills/viewcomponent-patterns/SKILL.md +0 -333
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d863378a0fb5338b1b1fe89c40c0c0ab9705b3ce60c45889eabf1ed626d27cf1
4
- data.tar.gz: d3a56da362430857991b85ab2b24ef10b5e2641dc312ecbe2a302bdca1a8352b
3
+ metadata.gz: 35e62d7d750d8d9fe1cff82806de1846d27fe505fad819551e48474f864b1499
4
+ data.tar.gz: 2f6889306b930aa78caec52cc92c7c6919ba379955a6d0484cc8bf44f52dcf6f
5
5
  SHA512:
6
- metadata.gz: 885cf285ecc91bf09f2bca2bd9f384cea7f36e1c966a0a74a20d7f87a9df8f69da79dd8363762111c3e206dfe1668ddbefc46cdda71c4e83eaeb138099e74a25
7
- data.tar.gz: b38579d624e4e66f5f051ce3aab2e61249fb549310e73e1959cdc0d67aef7fb63ff7c18232563269b2e826c5eb08f2a01bb5cf78f992df90bba3a0649d4d7a21
6
+ metadata.gz: 3ca0f0c0a9d6e1c2557c501a2a51bc9dee52e486b78cb78f1384232f5af704c209026e514f1dea2ea94c6962d502856add17c57b4afc79c462ed8fa39da8d236
7
+ data.tar.gz: ed49ec4ce672c5850b2bc3f3a6d73e31f1d08d6c8f7b1a779258db69ac9cdb48d1aa6e8478784a0f89bf161ec81bad31b79485455373a702240a1580ad31f6dc
@@ -157,6 +157,7 @@ Has `reset!` method. The `adapter=` setter validates against `VALID_ADAPTERS`.
157
157
  | `authorize_handler` | Handler/nil | `nil` | Authorization handler |
158
158
  | `current_user_method` | Symbol/nil | `nil` | Method name for current user |
159
159
  | `user_signed_in_method` | Symbol/nil | `nil` | Method name for signed-in check |
160
+ | `open_access` | Boolean | `false` | Opt out of the fail-closed access guard (demo/non-production only); ignored when a handler is configured |
160
161
 
161
162
  Has `reset!` method.
162
163
 
@@ -66,10 +66,17 @@ config.http.retry_max = 3
66
66
  ```
67
67
 
68
68
  ### Authentication (Devise)
69
+ SourceMonitor is **fail-closed by default**: with no `authenticate_with`/`authorize_with`
70
+ handler configured, every engine route returns `403 Forbidden`. Configure a handler
71
+ to protect the dashboard (the handler decides access and bypasses the fail-closed guard):
69
72
  ```ruby
70
73
  config.authentication.authenticate_with :authenticate_user!
71
74
  config.authentication.authorize_with ->(c) { c.current_user&.admin? }
72
75
  ```
76
+ For local demos/sandboxes only, opt out of the fail-closed guard (non-production):
77
+ ```ruby
78
+ config.authentication.open_access = true # default: false -- demo/non-production only
79
+ ```
73
80
 
74
81
  ### Image Downloads (Active Storage)
75
82
  ```ruby
@@ -167,7 +174,7 @@ end
167
174
  - [ ] Initializer exists at `config/initializers/source_monitor.rb`
168
175
  - [ ] Queue names match `config/queue.yml` (or `config/solid_queue.yml`) entries
169
176
  - [x] Dispatcher config includes `recurring_schedule: config/recurring.yml` (handled by install generator)
170
- - [ ] Authentication hooks configured for host auth system
177
+ - [ ] Authentication decision made: configure `authenticate_with`/`authorize_with` (fail-closed default) OR set `config.authentication.open_access = true` for demos only
171
178
  - [ ] HTTP timeouts appropriate for target feeds
172
179
  - [ ] Retention policy set for production
173
180
  - [ ] Workers restarted after configuration changes
@@ -283,10 +283,17 @@ config.realtime.solid_cable.connects_to = { database: { writing: :cable } }
283
283
 
284
284
  Class: `SourceMonitor::Configuration::AuthenticationSettings`
285
285
 
286
+ **Fail-closed by default.** When no `authenticate_with`/`authorize_with` handler is
287
+ configured and `open_access` is `false`, the engine denies every route with
288
+ `403 Forbidden` (see `SourceMonitor::Security::Authentication.access_denied_by_default?`).
289
+ Configuring either handler makes the handler authoritative and bypasses the
290
+ fail-closed guard.
291
+
286
292
  | Setting | Type | Default | Description |
287
293
  |---|---|---|---|
288
294
  | `current_user_method` | Symbol/nil | `nil` | Controller method to get current user |
289
295
  | `user_signed_in_method` | Symbol/nil | `nil` | Controller method to check sign-in status |
296
+ | `open_access` | Boolean | `false` | Opt out of the fail-closed guard so engine routes are public. **Demo/non-production only.** Ignored when a handler is configured. |
290
297
 
291
298
  ### Methods
292
299
 
@@ -312,6 +319,10 @@ config.authentication.authorize_with ->(controller) {
312
319
  config.authentication.authorize_with do
313
320
  redirect_to root_path unless current_user&.admin?
314
321
  end
322
+
323
+ # Open access (demo/non-production only) -- disables the fail-closed guard.
324
+ # Leave commented/false in production. A configured handler always wins.
325
+ config.authentication.open_access = true # default: false
315
326
  ```
316
327
 
317
328
  ---
@@ -119,7 +119,7 @@ After installation, review and customize the initializer. Key areas:
119
119
  | Section | Purpose |
120
120
  |---|---|
121
121
  | Queue settings | Queue names, concurrency, namespace |
122
- | Authentication | `authenticate_with`, `authorize_with` hooks |
122
+ | Authentication | **Fail-closed by default** -- configure `authenticate_with`/`authorize_with` hooks, or set `open_access = true` for demos only |
123
123
  | HTTP client | Timeouts, proxy, retries |
124
124
  | Fetching | Adaptive scheduling intervals and factors |
125
125
  | Health | Auto-pause/resume thresholds |
@@ -152,7 +152,17 @@ SOURCE_MONITOR_SETUP_TELEMETRY=true bin/source_monitor verify
152
152
  # Logs to log/source_monitor_setup.log
153
153
  ```
154
154
 
155
- ## Devise Integration
155
+ ## Authentication (Fail-Closed by Default)
156
+
157
+ SourceMonitor is **fail-closed by default**: with no `authenticate_with`/`authorize_with`
158
+ handler configured, every engine route (including create/update/delete/enqueue actions)
159
+ returns `403 Forbidden`. You must make one of two choices during setup:
160
+
161
+ 1. **Configure a handler** (recommended) -- the handler decides access and bypasses the
162
+ fail-closed guard.
163
+ 2. **Set `config.authentication.open_access = true`** -- opts out of the guard so routes
164
+ are public. **Demo/non-production only.** A configured handler always takes precedence
165
+ over this flag.
156
166
 
157
167
  When Devise is detected, the guided installer offers to wire authentication hooks:
158
168
 
@@ -238,6 +248,6 @@ end
238
248
  - [x] `Procfile.dev` includes `jobs:` entry for Solid Queue (handled by generator)
239
249
  - [x] Dispatcher config includes `recurring_schedule: config/recurring.yml` (handled by generator)
240
250
  - [ ] Solid Queue workers started
241
- - [ ] Authentication hooks configured in initializer
251
+ - [ ] Authentication decision made (engine is fail-closed by default): handler configured via `authenticate_with`/`authorize_with`, OR `config.authentication.open_access = true` for demos only
242
252
  - [ ] `bin/source_monitor verify` passes
243
253
  - [ ] Dashboard accessible at mount path
@@ -57,7 +57,12 @@ SourceMonitor.configure do |config|
57
57
  # ===========================================================================
58
58
  # Authentication
59
59
  # ===========================================================================
60
+ # SECURITY: SourceMonitor is FAIL-CLOSED by default. If you do not configure
61
+ # an authentication or authorization handler below, every engine route
62
+ # (including create/update/delete/enqueue actions) returns 403 Forbidden.
60
63
  # Handlers: Symbol (invoked on controller) or callable (receives controller).
64
+ # As soon as a handler is configured it decides access and the fail-closed
65
+ # guard no longer applies.
61
66
 
62
67
  # Authenticate before accessing any SourceMonitor page.
63
68
  # config.authentication.authenticate_with :authenticate_user!
@@ -71,6 +76,12 @@ SourceMonitor.configure do |config|
71
76
  # config.authentication.current_user_method = :current_user
72
77
  # config.authentication.user_signed_in_method = :user_signed_in?
73
78
 
79
+ # Explicit opt-in for open/unauthenticated access. Leave commented out in
80
+ # production. Only enable for local demos or sandboxes where engine routes
81
+ # are deliberately public.
82
+ # WARNING: non-production / demo only -- this disables the fail-closed guard.
83
+ # config.authentication.open_access = true
84
+
74
85
  # ===========================================================================
75
86
  # HTTP Client
76
87
  # ===========================================================================
@@ -57,6 +57,10 @@ bin/rails db:migrate
57
57
 
58
58
  ## Phase 5: Configure Authentication
59
59
 
60
+ **Fail-closed by default.** With no handler configured, every engine route returns
61
+ `403 Forbidden`. Make one of two choices: configure a handler (recommended) OR opt into
62
+ open access for demos only.
63
+
60
64
  Edit `config/initializers/source_monitor.rb`:
61
65
 
62
66
  ```ruby
@@ -68,10 +72,14 @@ SourceMonitor.configure do |config|
68
72
  }
69
73
  config.authentication.current_user_method = :current_user
70
74
  config.authentication.user_signed_in_method = :user_signed_in?
75
+
76
+ # OR, for local demos/sandboxes only (non-production) -- opt out of the
77
+ # fail-closed guard so routes are public. A configured handler always wins.
78
+ # config.authentication.open_access = true
71
79
  end
72
80
  ```
73
81
 
74
- - [ ] Authentication hook configured
82
+ - [ ] Authentication decision made: handler configured (fail-closed default) OR `open_access = true` set for demos only
75
83
  - [ ] Authorization hook configured (if needed)
76
84
 
77
85
  ## Phase 6: Configure Workers
@@ -2,6 +2,18 @@
2
2
 
3
3
  Version-specific migration notes for each major/minor version transition. Agents should reference this file when guiding users through multi-version upgrades.
4
4
 
5
+ ## 0.13.1 to 0.14.0
6
+
7
+ **Key changes:**
8
+ - **BREAKING — Fail-closed by default** (#129): engine routes return `403 Forbidden` unless `config.authentication.authenticate_with` or `config.authentication.authorize_with` is configured, OR `config.authentication.open_access = true` is explicitly set (non-production only).
9
+ - **Request flashes response-local** (#130): flash toasts no longer broadcast to the global `source_monitor_notifications` ActionCable stream; rendered inline on HTML loads and appended on turbo_stream responses instead. Background operational toasts stay global but context-free.
10
+ - **Gem package excludes `.claude` internals** (#131): only `.claude/skills/sm-*` shipped; agents, hooks, `settings.json`, commands, and non-`sm-*` skills excluded.
11
+
12
+ **Action items:**
13
+ 1. `bundle update source_monitor`
14
+ 2. **Action required (auth):** in `config/initializers/source_monitor.rb`, ensure at least one of `authenticate_with`, `authorize_with`, or `open_access = true` (demo/sandbox only). Without one, all engine routes return `403`.
15
+ 3. No migrations or other breaking API changes; flash/packaging changes apply transparently.
16
+
5
17
  ## 0.12.4 to 0.13.0
6
18
 
7
19
  **Key changes:**
data/CHANGELOG.md CHANGED
@@ -13,6 +13,21 @@ All notable changes to this project are documented below. The format follows [Ke
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ - No unreleased changes yet.
17
+
18
+ ## [0.14.0] - 2026-05-28
19
+
20
+ ### Security (BREAKING)
21
+ - **Fail-closed engine access by default** (#129). When the host app has configured no authentication/authorization handler, every mounted SourceMonitor route now returns `403 Forbidden` instead of being publicly accessible. Previously `authenticate!`/`authorize!` were silent no-ops when unconfigured, so create/update/delete/enqueue routes were public by omission.
22
+ - **Migration:** Configure a handler — `config.authentication.authenticate_with` and/or `config.authentication.authorize_with` — to gate the engine with your host auth stack. Configured handlers are honored exactly as before.
23
+ - **Demo opt-in:** To intentionally keep routes public (local demos/sandboxes only), set `config.authentication.open_access = true` (default `false`). Do not enable this in production.
24
+
25
+ ### Fixed
26
+ - **Request flashes no longer leak across users** (#130). Request flash toasts were broadcast to a global ActionCable stream (`source_monitor_notifications`) that every connected browser tab subscribed to, so one user's flash could appear on every user's screen. Flashes are now delivered response-local — rendered inline on full-page HTML loads and appended to the turbo_stream response otherwise — reaching only the requesting tab. Background operational toasts (fetch/scrape completion) remain global but carry no per-user request context.
27
+
28
+ ### Changed
29
+ - **Gem package no longer ships `.claude` internals** (#131). The packaged gem now excludes `.claude` agents, hooks, agent-memory, `settings.json`, commands, and non-`sm-*` skills; only the intended `.claude/skills/sm-*` SourceMonitor skills are shipped. Packaging is also now CWD-independent so the skill files are included deterministically.
30
+
16
31
  ## [0.13.1] - 2026-05-28
17
32
 
18
33
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- source_monitor (0.13.1)
4
+ source_monitor (0.14.0)
5
5
  cssbundling-rails (~> 1.4)
6
6
  faraday (~> 2.9)
7
7
  faraday-follow_redirects (~> 0.4)
data/README.md CHANGED
@@ -9,8 +9,8 @@ SourceMonitor is a production-ready Rails 8 mountable engine for ingesting, norm
9
9
  In your host Rails app:
10
10
 
11
11
  ```bash
12
- bundle add source_monitor --version "~> 0.13.1"
13
- # or add `gem "source_monitor", "~> 0.13.1"` manually, then run:
12
+ bundle add source_monitor --version "~> 0.14.0"
13
+ # or add `gem "source_monitor", "~> 0.14.0"` manually, then run:
14
14
  bundle install
15
15
  ```
16
16
 
@@ -46,7 +46,7 @@ This exposes `bin/source_monitor` (via Bundler binstubs) so you can run the guid
46
46
  Before running any SourceMonitor commands inside your host app, add the gem and install dependencies:
47
47
 
48
48
  ```bash
49
- bundle add source_monitor --version "~> 0.13.1"
49
+ bundle add source_monitor --version "~> 0.14.0"
50
50
  # or edit your Gemfile, then run
51
51
  bundle install
52
52
  ```
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.13.0
1
+ 0.14.0
@@ -4,11 +4,13 @@ module SourceMonitor
4
4
  class ApplicationController < ActionController::Base
5
5
  protect_from_forgery with: :exception, prepend: true
6
6
 
7
+ before_action :enforce_source_monitor_access_default
7
8
  before_action :authenticate_source_monitor_user
8
9
  before_action :authorize_source_monitor_access
9
10
 
10
- helper_method :source_monitor_current_user, :source_monitor_user_signed_in?
11
- after_action :broadcast_flash_toasts
11
+ helper_method :source_monitor_current_user, :source_monitor_user_signed_in?,
12
+ :source_monitor_flash_toasts
13
+ after_action :append_flash_toasts_to_turbo_stream
12
14
 
13
15
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
14
16
 
@@ -40,6 +42,30 @@ module SourceMonitor
40
42
  TOAST_DURATION_DEFAULT = 5000
41
43
  TOAST_DURATION_ERROR = 6000
42
44
 
45
+ # Fail-closed guard: when the host app has configured no authentication or
46
+ # authorization handler and has not explicitly opted into open access, deny
47
+ # all engine routes. Configured handlers short-circuit this and decide for
48
+ # themselves (see SourceMonitor::Security::Authentication).
49
+ def enforce_source_monitor_access_default
50
+ return unless SourceMonitor::Security::Authentication.access_denied_by_default?(self)
51
+
52
+ source_monitor_access_forbidden
53
+ end
54
+
55
+ def source_monitor_access_forbidden
56
+ message = "SourceMonitor access is not configured"
57
+ respond_to do |format|
58
+ format.html { render plain: message, status: :forbidden }
59
+ format.turbo_stream do
60
+ render turbo_stream: turbo_stream.append("flash",
61
+ partial: "source_monitor/shared/toast",
62
+ locals: { message: message, level: :error }),
63
+ status: :forbidden
64
+ end
65
+ format.json { render json: { error: message }, status: :forbidden }
66
+ end
67
+ end
68
+
43
69
  def authenticate_source_monitor_user
44
70
  SourceMonitor::Security::Authentication.authenticate!(self)
45
71
  end
@@ -60,22 +86,55 @@ module SourceMonitor
60
86
  level.to_sym == :error ? TOAST_DURATION_ERROR : TOAST_DURATION_DEFAULT
61
87
  end
62
88
 
63
- def broadcast_flash_toasts
64
- return if flash.empty?
65
- return unless request.format.html? || request.format.turbo_stream?
89
+ # Request flashes are delivered response-local, never broadcast over the
90
+ # global ActionCable notification stream. On full-page HTML loads the layout
91
+ # renders the toasts inline (see +source_monitor_flash_toasts+); on
92
+ # turbo_stream responses we append the toasts to the response body so they
93
+ # reach only the requesting tab.
94
+ def source_monitor_flash_toasts
95
+ payloads = flash_toast_payloads
96
+ # Reading via the layout consumes the flash for this request so it does
97
+ # not linger into the next one.
98
+ flash.discard unless payloads.empty?
99
+ payloads
100
+ end
66
101
 
67
- flash.each do |key, message|
68
- next if message.blank?
102
+ def append_flash_toasts_to_turbo_stream
103
+ return unless request.format.turbo_stream?
104
+ return if response.redirect?
105
+
106
+ payloads = flash_toast_payloads
107
+ return if payloads.empty?
108
+
109
+ streams = payloads.map do |payload|
110
+ view_context.turbo_stream.append(
111
+ "source_monitor_notifications",
112
+ partial: "source_monitor/shared/toast",
113
+ locals: {
114
+ message: payload[:message],
115
+ level: payload[:level],
116
+ delay_ms: toast_delay_for(payload[:level])
117
+ }
118
+ )
119
+ end
69
120
 
70
- Array(message).each do |msg|
71
- SourceMonitor::Realtime.broadcast_toast(
72
- message: msg,
73
- level: FLASH_LEVELS[key.to_sym] || :info
74
- )
121
+ response.body = "#{response.body}#{streams.join}"
122
+ flash.discard
123
+ end
124
+
125
+ # Builds the list of toast payloads ({ message:, level: }) for the current
126
+ # request's flash. Used by both the inline layout renderer and the
127
+ # turbo_stream after_action so request flashes stay response-local.
128
+ def flash_toast_payloads
129
+ return [] if flash.empty?
130
+
131
+ flash.flat_map do |key, message|
132
+ Array(message).filter_map do |msg|
133
+ next if msg.blank?
134
+
135
+ { message: msg, level: FLASH_LEVELS[key.to_sym] || :info }
75
136
  end
76
137
  end
77
- ensure
78
- flash.discard
79
138
  end
80
139
  end
81
140
  end
@@ -19,6 +19,12 @@
19
19
  <div id="source_monitor_notifications"
20
20
  data-notification-container-target="list"
21
21
  class="flex w-full flex-col gap-3">
22
+ <%# Request flashes render response-local here (never broadcast to all tabs). %>
23
+ <% source_monitor_flash_toasts.each do |toast| %>
24
+ <%= render "source_monitor/shared/toast",
25
+ message: toast[:message],
26
+ level: toast[:level] %>
27
+ <% end %>
22
28
  </div>
23
29
  <div data-notification-container-target="badge"
24
30
  class="pointer-events-auto hidden">
@@ -137,6 +137,11 @@ Call `config.realtime.action_cable_config` if you need a full hash for environme
137
137
 
138
138
  ## Authentication Helpers
139
139
 
140
+ **Fail-closed by default.** SourceMonitor denies access to every engine route
141
+ (returning `403 Forbidden`) unless you configure an authentication or
142
+ authorization handler. This prevents the engine's create/update/delete/enqueue
143
+ routes from being public by accident.
144
+
140
145
  Protect the dashboard with host-specific auth in one place:
141
146
 
142
147
  ```ruby
@@ -148,7 +153,19 @@ config.authentication.current_user_method = :current_user
148
153
  config.authentication.user_signed_in_method = :user_signed_in?
149
154
  ```
150
155
 
151
- Handlers can be symbols (invoked on the controller) or callables. Return `false` or raise to deny access.
156
+ Handlers can be symbols (invoked on the controller) or callables. Return `false` or raise to deny access. As soon as either handler is configured, the handler decides access and the fail-closed guard no longer applies.
157
+
158
+ ### Open access opt-in (non-production)
159
+
160
+ For local demos or sandboxes where engine routes are deliberately public, you
161
+ can explicitly opt out of the fail-closed guard:
162
+
163
+ ```ruby
164
+ config.authentication.open_access = true # default: false
165
+ ```
166
+
167
+ This is intended for non-production/demo environments only. Configuring a
168
+ handler always takes precedence over this flag.
152
169
 
153
170
  ## Health Model
154
171
 
data/docs/deployment.md CHANGED
@@ -33,7 +33,7 @@ SourceMonitor assumes the standard Rails 8 process split:
33
33
 
34
34
  ## Security & Authentication
35
35
 
36
- - Lock down the engine routes with authentication hooks (`config.authentication.authenticate_with` / `authorize_with`).
36
+ - SourceMonitor is **fail-closed by default**: without a configured handler every engine route returns `403 Forbidden`. Lock down the routes with authentication hooks (`config.authentication.authenticate_with` / `authorize_with`). Only set `config.authentication.open_access = true` for non-production demos where public access is intentional.
37
37
  - Configure HTTPS for Action Cable if you expose Solid Cable over the public internet.
38
38
  - Store API keys for authenticated feeds in encrypted credentials and inject them via per-source custom headers.
39
39