mixin_bot 3.0.0 → 3.0.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +1 -1
  3. data/API_COVERAGE.md +1 -0
  4. data/CHANGELOG.md +11 -1
  5. data/README.md +75 -1
  6. data/docs/agent/cookbook.md +23 -0
  7. data/lib/generators/mixin_bot/authentication/USAGE +29 -0
  8. data/lib/generators/mixin_bot/authentication/authentication_generator.rb +82 -0
  9. data/lib/generators/mixin_bot/authentication/templates/app/controllers/concerns/authentication.rb.tt +52 -0
  10. data/lib/generators/mixin_bot/authentication/templates/app/controllers/sessions_controller.rb.tt +24 -0
  11. data/lib/generators/mixin_bot/authentication/templates/app/models/current.rb.tt +4 -0
  12. data/lib/generators/mixin_bot/authentication/templates/app/models/session.rb.tt +3 -0
  13. data/lib/generators/mixin_bot/authentication/templates/app/models/user.rb.tt +12 -0
  14. data/lib/generators/mixin_bot/authentication/templates/app/views/sessions/new.html.erb.tt +3 -0
  15. data/lib/generators/mixin_bot/authentication/templates/config/initializers/omniauth.rb.tt +21 -0
  16. data/lib/generators/mixin_bot/blaze/USAGE +19 -0
  17. data/lib/generators/mixin_bot/blaze/blaze_generator.rb +43 -0
  18. data/lib/generators/mixin_bot/blaze/templates/app/mixin/handlers/text_handler.rb +14 -0
  19. data/lib/generators/mixin_bot/blaze/templates/config/initializers/mixin_bot_blaze.rb +23 -0
  20. data/lib/generators/mixin_bot/install/USAGE +24 -0
  21. data/lib/generators/mixin_bot/install/install_generator.rb +50 -0
  22. data/lib/generators/mixin_bot/notifications/USAGE +13 -0
  23. data/lib/generators/mixin_bot/notifications/notifications_generator.rb +49 -0
  24. data/lib/generators/mixin_bot/notifications/templates/app/notifications/payment_received_notification.rb +24 -0
  25. data/lib/generators/mixin_bot/outputs/USAGE +29 -0
  26. data/lib/generators/mixin_bot/outputs/outputs_generator.rb +50 -0
  27. data/lib/generators/mixin_bot/outputs/templates/app/mixin/processors/deposit_processor.rb +21 -0
  28. data/lib/generators/mixin_bot/outputs/templates/app/models/mixin_output.rb +8 -0
  29. data/lib/generators/mixin_bot/outputs/templates/db/migrate/create_mixin_outputs.rb.tt +26 -0
  30. data/lib/generators/mixin_bot/transfers/USAGE +37 -0
  31. data/lib/generators/mixin_bot/transfers/templates/app/jobs/mixin_transfers/perform_job.rb +13 -0
  32. data/lib/generators/mixin_bot/transfers/templates/app/jobs/mixin_transfers/reconcile_job.rb +14 -0
  33. data/lib/generators/mixin_bot/transfers/templates/app/models/mixin_transfer.rb +11 -0
  34. data/lib/generators/mixin_bot/transfers/templates/db/migrate/create_mixin_transfers.rb.tt +23 -0
  35. data/lib/generators/mixin_bot/transfers/transfers_generator.rb +67 -0
  36. data/lib/generators/mixin_bot.rb +10 -0
  37. data/lib/mixin_bot/api/app.rb +20 -0
  38. data/lib/mixin_bot/api/snapshot.rb +5 -0
  39. data/lib/mixin_bot/blaze/reactor.rb +8 -3
  40. data/lib/mixin_bot/blaze/router.rb +333 -0
  41. data/lib/mixin_bot/cache_store_adapter.rb +3 -1
  42. data/lib/mixin_bot/client.rb +2 -1
  43. data/lib/mixin_bot/computer.rb +3 -1
  44. data/lib/mixin_bot/json_decoder.rb +14 -0
  45. data/lib/mixin_bot/notifications/mixin_channel.rb +80 -0
  46. data/lib/mixin_bot/outputs/cursor_model.rb +33 -0
  47. data/lib/mixin_bot/outputs/envelope.rb +160 -0
  48. data/lib/mixin_bot/outputs/memory_stores.rb +97 -0
  49. data/lib/mixin_bot/outputs/poller.rb +178 -0
  50. data/lib/mixin_bot/outputs/processing_job.rb +54 -0
  51. data/lib/mixin_bot/outputs/processors.rb +124 -0
  52. data/lib/mixin_bot/outputs/receipt_model.rb +86 -0
  53. data/lib/mixin_bot/outputs.rb +42 -0
  54. data/lib/mixin_bot/rails/railtie.rb +44 -0
  55. data/lib/mixin_bot/rails/tasks/poller.rake +33 -0
  56. data/lib/mixin_bot/rails.rb +34 -0
  57. data/lib/mixin_bot/registry.rb +79 -0
  58. data/lib/mixin_bot/transfers/model.rb +112 -0
  59. data/lib/mixin_bot/transfers/performer.rb +64 -0
  60. data/lib/mixin_bot/transfers/reconcile.rb +53 -0
  61. data/lib/mixin_bot/transfers.rb +23 -0
  62. data/lib/mixin_bot/version.rb +1 -1
  63. data/lib/mixin_bot.rb +3 -0
  64. data/llms.txt +1 -1
  65. metadata +51 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c6e5421b12eabf2c32c0500c1b828228fd24a10855047f2f7f1bf605dc62ada
4
- data.tar.gz: a18da86395abe9053e52d70965538359cbbada5bc13439196c73536382d05c87
3
+ metadata.gz: 520bf980215da4e70c9fa6ae1d9297874fc16fc8264a43bb019a126f0fb0d555
4
+ data.tar.gz: 1d5cf99a0e645d4ae110e73148b501bd5f942b3f2c0e9ac0a4338d2d38699a7a
5
5
  SHA512:
6
- metadata.gz: 4a20f4ef42062cff12ff5ecb280f8d447528ee2f7882e0425783af545b57a3fe7faecf36ca7c70387e5c3dbeb08cdcea02729b919cddadb386fe76b5b6d45fa6
7
- data.tar.gz: f9f88a933a44502b2709fd282c8170adf73653d46721c83044358b59434444b10efac60ef82b84cb470cffaf7d4694d7bbeb270dc851c54ba5a66e40ff285713
6
+ metadata.gz: b73f1bd21043e018410555d93a537b2314f3398a174bf7ce443cce1652b578e425569faa9118d413789607f106863dbf08b1b79ef4da643f28c40fdbad065479
7
+ data.tar.gz: 49b66d6300397db22af3f28688eca9999a68ad47f07689a84f94a6c43831d96e0ad42c88d7b95cb3a12f531ab4bf4d5540f84cb0f070508687027f20337ff36a
data/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md — MixinBot
2
2
 
3
- Ruby gem (v3.0.0): Mixin Network REST SDK + `mixinbot` CLI. Parity targets: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client), [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client).
3
+ Ruby gem (v3.0.2): Mixin Network REST SDK + `mixinbot` CLI. Parity targets: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client), [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client).
4
4
 
5
5
  ## Commands
6
6
 
data/API_COVERAGE.md CHANGED
@@ -81,6 +81,7 @@ Status values: `done` | `alias` | `n/a` (CLI-only / config / platform-specific)
81
81
  | **Snapshots** |
82
82
  | SafeSnapshots* | `API#safe_snapshots` | GET `/safe/snapshots` | done |
83
83
  | SafeSnapshotById | `API#safe_snapshot` | GET `/safe/snapshots/:id` | done |
84
+ | SafeSnapshotByTrace | `API#safe_snapshot_by_trace` | GET `/safe/snapshots/trace/:trace_id` | done |
84
85
  | SafeNotifySnapshot | `API#create_safe_snapshot_notification` | POST notifications | done |
85
86
  | Snapshots / SnapshotById / SnapshotByTraceId | `API#snapshots`, `#snapshot`, `#snapshot_by_trace_id` | legacy | done |
86
87
  | NetworkSnapshot* | `API#network_snapshot(s)` | legacy | done |
data/CHANGELOG.md CHANGED
@@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [3.0.2] - 2026-09-24
9
+
10
+ ### Added
11
+
12
+ - **`MixinBot::API#update_app_security`** — `POST /apps/:id/security`, the endpoint that persists an app's `allowed_ips` and `resource_patterns`. The generic `update_app` endpoint silently ignores both fields, so code that tried to manage the resource-pattern whitelist through it was a no-op.
13
+
14
+ ## [3.0.1] - 2026-09-19
15
+
16
+ ### Fixed
17
+
18
+ - **Blaze reconnect backoff no longer resets on short-lived connections.** `Reactor::STABLE_PERIOD` (the lifetime a connection needs for the backoff to reset) was exactly `MAX_BACKOFF` (30s), so any failure mode that closes sessions at a ~30s lifetime (e.g. a gateway throttling abusive reconnect patterns) read as "stable" on every cycle — the backoff pinned at the initial 1s and the reactor reconnected hot indefinitely, which is itself the abuse pattern that triggers such throttling. `STABLE_PERIOD` is now 120s (well above every single backoff step) and `MAX_BACKOFF` grows the ceiling to 300s so a sustained failure mode backs off hard while a single transient blip still reconnects in ~1s.
9
19
 
10
20
  ## [3.0.0] - 2026-09-15
11
21
 
data/README.md CHANGED
@@ -6,7 +6,7 @@ Ruby SDK and CLI for [Mixin Network](https://developers.mixin.one/docs): authent
6
6
 
7
7
  The gem aims for **parity with the official [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client)** Go SDK and **[bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client)** Node SDK. See [API_COVERAGE.md](API_COVERAGE.md) for the full mapping; run `rake mixin_bot:api_coverage` to confirm no gaps are marked missing.
8
8
 
9
- Current gem version: **3.0.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
9
+ Current gem version: **3.0.2** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
10
10
 
11
11
  ## Requirements
12
12
 
@@ -327,6 +327,80 @@ Operational notes:
327
327
  - **Phased restarts keep the old handler code** in the hosting process until a full restart; the same applies to Solid Queue's plugin.
328
328
  - Workers that need to *send* messages should use the REST API (`create_message` / `create_messages`), not the socket.
329
329
 
330
+ ## Rails integration
331
+
332
+ The gem ships Rails generators under the `mixin_bot:` namespace. The first one sets up **"Sign in with Mixin"** (OAuth login via [`omniauth-mixin`](https://github.com/an-lee/omniauth-mixin)), modeled on Rails' built-in `rails g authentication`:
333
+
334
+ ```bash
335
+ rails generate mixin_bot:authentication
336
+ ```
337
+
338
+ This creates an identity-only Mixin login: OmniAuth initializer, `User` / `Session` / `Current` models, `SessionsController` with the OAuth callback, an `Authentication` controller concern (injected into `ApplicationController`), a sign-in view, routes, and the `CreateUsers` / `CreateSessions` migrations. It also adds `omniauth-mixin` and `omniauth-rails_csrf_protection` to the Gemfile (uncommenting existing entries, otherwise `bundle add`) and runs migrations generation. Run `bin/rails db:migrate` afterwards.
339
+
340
+ Then:
341
+
342
+ - Set `MIXIN_CLIENT_ID` and `MIXIN_CLIENT_SECRET` from the [Mixin Developer Dashboard](https://developers.mixin.one/dashboard).
343
+ - Register `https://<your-host>/auth/mixin/callback` as the OAuth redirect URL of your Mixin app.
344
+
345
+ Notes:
346
+
347
+ - **Identity only** — the OAuth exchange is used to establish identity (Mixin user id + name + avatar). Access/refresh tokens are not persisted; a future generator may add API-access wiring on top.
348
+ - **Fail loud at sign-in, not at boot** — the app boots fine without credentials (deploys, CI, `assets:precompile`); attempting to sign in raises an error naming the missing `MIXIN_CLIENT_ID` / `MIXIN_CLIENT_SECRET`.
349
+ - **CSRF-protected request phase** — the sign-in button POSTs to `/auth/mixin` (OmniAuth 2.x default request method, protected by `omniauth-rails_csrf_protection`); the provider redirects back to the `/auth/mixin/callback` GET route.
350
+ - **Existing apps beware** — like `rails g authentication`, the generator templates over target files (e.g. an existing `User` model) and will prompt on conflicts; reconcile manually if you already have users.
351
+ - Sessions are database-backed (`sessions` table with `ip_address` / `user_agent`) with a signed httponly cookie, exactly like Rails 8's `authentication` generator; `Current.user` is available after sign-in.
352
+
353
+ The generated code depends only on Rails and the two OmniAuth gems — not on `mixin_bot` itself — so the login keeps working even if you later drop the gem from the app.
354
+
355
+ ### The full Rails integration layer (`require 'mixin_bot/rails'`)
356
+
357
+ Beyond the generators, the gem ships an optional runtime integration layer. It is opt-in and Rails stays a soft dependency:
358
+
359
+ ```ruby
360
+ # config/application.rb, after Bundler.require
361
+ require 'mixin_bot/rails'
362
+ ```
363
+
364
+ Or scaffold everything in one command and skip pieces as needed:
365
+
366
+ ```bash
367
+ rails generate mixin_bot:install
368
+ rails generate mixin_bot:install --skip-notifications --skip-transfers
369
+ ```
370
+
371
+ What each piece gives you:
372
+
373
+ - **Blaze message routing** — `MixinBot::Blaze::Router` is a callable that plugs into `config.blaze_handler`. Handlers live in `app/mixin/handlers`, receive a decoded `Message`, and reply over the HTTP message API (no WebSocket access needed):
374
+
375
+ ```ruby
376
+ # config/initializers/mixin_bot_blaze.rb (generated by `rails g mixin_bot:blaze`)
377
+ Rails.application.config.to_prepare do
378
+ MixinBot.configure do
379
+ self.blaze_handler = MixinBot::Blaze::Router.new do
380
+ on 'text', Mixin::Handlers::TextHandler # 'text' => PLAIN_TEXT
381
+ on 'transfer', Mixin::Handlers::TransferHandler
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ Pair with the Puma plugin (`plugin :mixin_blaze`) so messages are received inside the web process tree. Per-bot: `MixinBot::Blaze::Router.new(api: MixinBot.bot(:shop))`.
388
+
389
+ - **Outputs polling** — `rails g mixin_bot:outputs` creates a receipt table (unique per `bot_app_id` + `output_id` — the dedup gate) and an example processor in `app/mixin/processors`. The poller's resume cursor is derived from the receipts themselves (newest recorded output `sequence` per bot — unique, monotonic, and the key the outputs API paginates on) — no separate cursor table. Run `rails mixin_bot:poller` (standalone process; pass `[bot_name]` for a registered bot). Every output — inbound *and* outbound — is delivered to matching processors through your job queue; memos bridge lazily via the snapshot notification API. Processors implement `self.matches?(envelope)` + `#process` and should be idempotent (dispatch is at-least-once).
390
+
391
+ - **Outbound transfers** — `rails g mixin_bot:transfers` creates a `MixinTransfer` ledger with the state machine (`pending → broadcast → confirmed | failed | reconciling`). Creating a record enqueues the Safe pipeline with the record's `trace_id` as the request id — retries are network-deduplicated. A timeout after submission lands in `reconciling`; the recurring `MixinTransfers.ReconcileJob` confirms from the trace snapshot or re-sends safely.
392
+
393
+ - **Mixin notifications** — `rails g mixin_bot:notifications` adds `noticed` and an example notification with `deliver_by :mixin`. The channel (`MixinBot::Notifications::MixinChannel`) sends text/post/app-card messages with the configured bot's credentials; it loads only when Noticed is present.
394
+
395
+ Multiple bots in one process: register named bots, then reference them everywhere (`MixinBot.bot(:shop)`, `mixin_bot:poller[shop]`, `Router.new(api: ...)`, `deliver_by :mixin, bot: :shop`):
396
+
397
+ ```ruby
398
+ MixinBot.register_bot :shop,
399
+ app_id: '...', session_id: '...',
400
+ session_private_key: '...', server_public_key: '...',
401
+ spend_key: '...'
402
+ ```
403
+
330
404
  ## Deep links and bot auth
331
405
 
332
406
  ```ruby
@@ -156,6 +156,29 @@ Operational notes:
156
156
  - **Phased restarts keep old handler code** in the hosting process until a full restart.
157
157
  - Workers that need to *send* messages use the REST API (`create_message`), not the socket.
158
158
 
159
+ ## Mixin login for a Rails app (`rails g mixin_bot:authentication`)
160
+
161
+ Task: add "Sign in with Mixin" (OAuth identity login) to an existing Rails app.
162
+
163
+ ```bash
164
+ bundle add mixin_bot # if not already present
165
+ rails generate mixin_bot:authentication
166
+ bin/rails db:migrate
167
+ ```
168
+
169
+ Generated: `config/initializers/omniauth.rb` (OmniAuth `:mixin` provider), `app/models/{user,session,current}.rb`, `app/controllers/sessions_controller.rb` + `app/controllers/concerns/authentication.rb` (injected into `ApplicationController`), `app/views/sessions/new.html.erb`, `resource :session` + `/auth/mixin/callback` routes, and `CreateUsers` (`mixin_user_id` unique, `name`, `avatar_url`) / `CreateSessions` migrations. The Gemfile gains `omniauth-mixin` and `omniauth-rails_csrf_protection`.
170
+
171
+ Required environment: `MIXIN_CLIENT_ID`, `MIXIN_CLIENT_SECRET` (from the Mixin Developer Dashboard); register `https://<host>/auth/mixin/callback` as the OAuth redirect URL.
172
+
173
+ Behavior notes for agents:
174
+
175
+ - **Identity only** — sign-in stores the Mixin user id + name + avatar (`User.find_or_create_by_mixin_auth!`, matched by uid). OAuth access/refresh tokens are never persisted; do not add token columns to the generated models.
176
+ - **Boot is safe without credentials** (CI / `assets:precompile`); a sign-in attempt without them raises `ArgumentError` naming the missing env vars (OmniAuth setup phase).
177
+ - **Request phase is POST + CSRF** (`button_to` → `/auth/mixin`, `omniauth-rails_csrf_protection`); callback is the GET route → `sessions#create`. A GET to `/auth/mixin` is rejected (OmniAuth 2.x).
178
+ - **Sessions** are DB-backed (`sessions` rows, `ip_address`/`user_agent`) remembered in a signed httponly cookie; `Current.user` after sign-in; unauthenticated requests redirect to the sign-in page and return after login; `terminate_session` on sign-out.
179
+ - **Self-contained** — generated code references no `mixin_bot` APIs; the login survives removing the gem. Re-running the generator does not duplicate routes or Gemfile entries; template conflicts prompt like any Rails generator (an existing `User` model is overwritten only if you choose so).
180
+ - To call Mixin APIs on behalf of a signed-in user afterwards, use the gem's OAuth machinery (`MixinBot::API` with delegated authorization) separately from the generated authentication code.
181
+
159
182
  ## Discover API methods
160
183
 
161
184
  ```bash
@@ -0,0 +1,29 @@
1
+ Description:
2
+ Adds "Sign in with Mixin" to this Rails application using OmniAuth
3
+ (omniauth-mixin): User/Session/Current models, a sessions controller with
4
+ the OAuth callback, an authentication concern, routes, migrations, and
5
+ the OmniAuth initializer.
6
+
7
+ The generated login is identity-only: the OAuth access token is used to
8
+ establish identity and is not persisted.
9
+
10
+ After generating, set MIXIN_CLIENT_ID and MIXIN_CLIENT_SECRET, and register
11
+ https://<your-host>/auth/mixin/callback as the OAuth redirect URL in the
12
+ Mixin developer dashboard.
13
+
14
+ Example:
15
+ rails generate mixin_bot:authentication
16
+
17
+ This will create:
18
+ config/initializers/omniauth.rb
19
+ app/models/user.rb
20
+ app/models/session.rb
21
+ app/models/current.rb
22
+ app/controllers/sessions_controller.rb
23
+ app/controllers/concerns/authentication.rb
24
+ app/views/sessions/new.html.erb
25
+ db/migrate/xxx_create_users.rb
26
+ db/migrate/xxx_create_sessions.rb
27
+
28
+ It will also add `omniauth-mixin` and `omniauth-rails_csrf_protection` to
29
+ the Gemfile and inject `include Authentication` into ApplicationController.
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module MixinBot
6
+ module Generators
7
+ # `rails generate mixin_bot:authentication` — sets up "Sign in with Mixin"
8
+ # via OmniAuth, modeled on Rails' built-in `authentication` generator.
9
+ # Identity only: the OAuth tokens are used to establish identity and are
10
+ # not persisted.
11
+ class AuthenticationGenerator < Rails::Generators::Base
12
+ # Rails 8.1 moved bundle_command out of Actions into BundleHelper.
13
+ unless method_defined?(:bundle_command) || private_method_defined?(:bundle_command)
14
+ begin
15
+ require 'rails/generators/bundle_helper'
16
+ include Rails::Generators::BundleHelper
17
+ rescue LoadError
18
+ # Older Rails ships bundle_command in Rails::Generators::Actions.
19
+ end
20
+ end
21
+
22
+ source_root File.expand_path('templates', __dir__)
23
+
24
+ SESSION_ROUTE = 'resource :session, only: [:new, :destroy]'
25
+ CALLBACK_ROUTE = "get '/auth/mixin/callback', to: 'sessions#create'"
26
+ OMNIAUTH_GEMS = %w[omniauth-mixin omniauth-rails_csrf_protection].freeze
27
+
28
+ def create_authentication_files
29
+ template 'config/initializers/omniauth.rb'
30
+ template 'app/models/user.rb'
31
+ template 'app/models/session.rb'
32
+ template 'app/models/current.rb'
33
+ template 'app/controllers/sessions_controller.rb'
34
+ template 'app/controllers/concerns/authentication.rb'
35
+ template 'app/views/sessions/new.html.erb'
36
+ end
37
+
38
+ def configure_application_controller
39
+ inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', " include Authentication\n"
40
+ end
41
+
42
+ def configure_authentication_routes
43
+ route SESSION_ROUTE
44
+ route CALLBACK_ROUTE
45
+ end
46
+
47
+ def add_migrations
48
+ generate 'migration', 'CreateUsers', 'mixin_user_id:string!:uniq name:string avatar_url:string', '--force'
49
+ generate 'migration', 'CreateSessions', 'user:references ip_address:string user_agent:string', '--force'
50
+ end
51
+
52
+ def add_omniauth_gems
53
+ gemfile = File.read(gemfile_path)
54
+ uncommented = []
55
+
56
+ OMNIAUTH_GEMS.each do |name|
57
+ pattern = /gem\s+["']#{Regexp.escape(name)}["']/
58
+ next if gemfile.match?(/^\s*#{pattern}/) # already present
59
+
60
+ if gemfile.match?(/^\s*#\s*#{pattern}/)
61
+ uncomment_lines 'Gemfile', pattern
62
+ uncommented << name
63
+ else
64
+ # Single positional call: bundle_command signatures differ across
65
+ # railties versions (8.1 BundleHelper is (command, env, params);
66
+ # older Actions is (command, options)) — only this shape is valid
67
+ # on all of them, and bundler handles --quiet itself.
68
+ bundle_command("add #{name} --quiet")
69
+ end
70
+ end
71
+
72
+ bundle_command('install --quiet') unless uncommented.empty?
73
+ end
74
+
75
+ private
76
+
77
+ def gemfile_path
78
+ File.expand_path('Gemfile', destination_root)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,52 @@
1
+ module Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :require_authentication
6
+ helper_method :authenticated?
7
+ end
8
+
9
+ class_methods do
10
+ def allow_unauthenticated_access(**options)
11
+ skip_before_action :require_authentication, **options
12
+ end
13
+ end
14
+
15
+ private
16
+ def authenticated?
17
+ resume_session
18
+ end
19
+
20
+ def require_authentication
21
+ resume_session || request_authentication
22
+ end
23
+
24
+ def resume_session
25
+ Current.session ||= find_session_by_cookie
26
+ end
27
+
28
+ def find_session_by_cookie
29
+ Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
30
+ end
31
+
32
+ def request_authentication
33
+ session[:return_to_after_authenticating] = request.url
34
+ redirect_to new_session_path
35
+ end
36
+
37
+ def after_authentication_url
38
+ session.delete(:return_to_after_authenticating) || root_url
39
+ end
40
+
41
+ def start_new_session_for(user)
42
+ user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
43
+ Current.session = session
44
+ cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
45
+ end
46
+ end
47
+
48
+ def terminate_session
49
+ Current.session&.destroy
50
+ cookies.delete(:session_id)
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ class SessionsController < ApplicationController
2
+ allow_unauthenticated_access only: %i[new create]
3
+
4
+ def new
5
+ end
6
+
7
+ def create
8
+ auth = request.env["omniauth.auth"]
9
+
10
+ unless auth
11
+ redirect_to new_session_path, alert: "Sign in with Mixin failed. Please try again."
12
+ return
13
+ end
14
+
15
+ user = User.find_or_create_by_mixin_auth!(auth)
16
+ start_new_session_for user
17
+ redirect_to after_authentication_url
18
+ end
19
+
20
+ def destroy
21
+ terminate_session
22
+ redirect_to new_session_path, status: :see_other
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :session
3
+ delegate :user, to: :session, allow_nil: true
4
+ end
@@ -0,0 +1,3 @@
1
+ class Session < ApplicationRecord
2
+ belongs_to :user
3
+ end
@@ -0,0 +1,12 @@
1
+ class User < ApplicationRecord
2
+ has_many :sessions, dependent: :destroy
3
+
4
+ # Finds or creates the user for an OmniAuth auth hash and refreshes the
5
+ # stored Mixin profile (name, avatar). The OAuth exchange is used only to
6
+ # establish identity; credentials are not stored.
7
+ def self.find_or_create_by_mixin_auth!(auth)
8
+ user = find_or_create_by!(mixin_user_id: auth.uid)
9
+ user.update!(name: auth.info.name, avatar_url: auth.info.image)
10
+ user
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ <p style="color: red"><%%= alert %></p>
2
+
3
+ <%%= button_to "Sign in with Mixin", "/auth/mixin", data: { turbo: false } %>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth"
4
+ require "omniauth/strategies/mixin"
5
+
6
+ Rails.application.config.middleware.use OmniAuth::Builder do
7
+ provider :mixin,
8
+ ENV.fetch("MIXIN_CLIENT_ID", nil),
9
+ ENV.fetch("MIXIN_CLIENT_SECRET", nil),
10
+ setup: lambda { |env|
11
+ client_id = ENV["MIXIN_CLIENT_ID"]
12
+ client_secret = ENV["MIXIN_CLIENT_SECRET"]
13
+
14
+ if client_id.blank? || client_secret.blank?
15
+ raise ArgumentError, "Missing Mixin OAuth credentials: set MIXIN_CLIENT_ID and MIXIN_CLIENT_SECRET"
16
+ end
17
+
18
+ env["omniauth.strategy"].options[:client_id] = client_id
19
+ env["omniauth.strategy"].options[:client_secret] = client_secret
20
+ }
21
+ end
@@ -0,0 +1,19 @@
1
+ Description:
2
+ Adds Mixin Blaze message routing to this Rails application: a routes
3
+ initializer wired to MixinBot's `blaze_handler`, an example text handler
4
+ under app/mixin/handlers, and the `mixin_blaze` Puma plugin line in
5
+ config/puma.rb so messages are received inside the web process tree.
6
+
7
+ Requires MixinBot bot credentials (app_id, session_id,
8
+ session_private_key, server_public_key) to be configured, e.g. from
9
+ environment variables.
10
+
11
+ Example:
12
+ rails generate mixin_bot:blaze
13
+
14
+ This will create:
15
+ config/initializers/mixin_bot_blaze.rb
16
+ app/mixin/handlers/text_handler.rb
17
+
18
+ It will also add `plugin :mixin_blaze` to config/puma.rb (creating it
19
+ when missing) without duplicating an existing line.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module MixinBot
6
+ module Generators
7
+ # `rails generate mixin_bot:blaze` — scaffolds Mixin Blaze message
8
+ # routing: a routes initializer wired to `config.blaze_handler`, an
9
+ # example text handler under `app/mixin/handlers`, and the `mixin_blaze`
10
+ # Puma plugin line in `config/puma.rb` so the app receives messages in
11
+ # its own process tree.
12
+ class BlazeGenerator < Rails::Generators::Base
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ PUMA_PLUGIN_LINE = 'plugin :mixin_blaze'
16
+ PUMA_COMMENT = '# Receive Mixin Blaze messages in the web process tree (MixinBot).'
17
+
18
+ def create_routes_initializer
19
+ template 'config/initializers/mixin_bot_blaze.rb'
20
+ end
21
+
22
+ def create_example_handler
23
+ template 'app/mixin/handlers/text_handler.rb'
24
+ end
25
+
26
+ def configure_puma_plugin
27
+ if File.exist?(puma_path)
28
+ return if File.read(puma_path).match?(/^\s*plugin\s+:mixin_blaze\b/)
29
+
30
+ append_to_file 'config/puma.rb', "\n#{PUMA_COMMENT}\n#{PUMA_PLUGIN_LINE}\n"
31
+ else
32
+ create_file 'config/puma.rb', "#{PUMA_COMMENT}\n#{PUMA_PLUGIN_LINE}\n"
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def puma_path
39
+ File.expand_path('config/puma.rb', destination_root)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mixin
4
+ module Handlers
5
+ # Example Blaze handler: echoes text messages back to the sender.
6
+ # Handlers are instantiated per message; `message` exposes decoded data,
7
+ # conversation/sender ids, and the `reply` helper (HTTP message API).
8
+ class TextHandler < MixinBot::Blaze::Router::Base
9
+ def handle
10
+ reply("You said: #{message.data}")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixin Blaze message routing.
4
+ #
5
+ # Requires bot credentials in MixinBot.configure (app_id, session_id,
6
+ # session_private_key, server_public_key, spend_key) — typically from
7
+ # environment variables — and the `mixin_blaze` Puma plugin in
8
+ # config/puma.rb to actually receive messages.
9
+ #
10
+ # Routes dispatch decoded envelopes in registration order (first match
11
+ # wins); handlers live under app/mixin/handlers and are re-resolved on
12
+ # every code reload in development.
13
+
14
+ Rails.application.config.to_prepare do
15
+ MixinBot.configure do
16
+ self.blaze_handler = MixinBot::Blaze::Router.new do
17
+ on 'text', Mixin::Handlers::TextHandler
18
+ # on 'transfer', Mixin::Handlers::TransferHandler
19
+ # on category: 'APP_CARD', handler: Mixin::Handlers::AppCardHandler
20
+ # on action: 'CREATE_MESSAGE', handler: ->(message) { ... }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ Description:
2
+ Sets up a Mixin Messenger/Network Rails application in one command:
3
+ "Sign in with Mixin" (OmniAuth), Blaze message routing with the Puma
4
+ plugin, the outputs poller with an example processor, the outbound
5
+ transfer ledger, and Noticed-driven Mixin notifications.
6
+
7
+ Skip any component:
8
+
9
+ rails generate mixin_bot:install --skip-notifications --skip-transfers
10
+ rails generate mixin_bot:install --skip notifications,transfers
11
+ rails generate mixin_bot:install --skip authentication
12
+
13
+ Example:
14
+ rails generate mixin_bot:install
15
+
16
+ This will run, in order:
17
+ rails generate mixin_bot:authentication
18
+ rails generate mixin_bot:blaze
19
+ rails generate mixin_bot:outputs
20
+ rails generate mixin_bot:transfers
21
+ rails generate mixin_bot:notifications
22
+
23
+ Afterwards: set your bot credentials, run `rails db:migrate`, and start
24
+ the poller (`rails mixin_bot:poller`) and your job backend.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module MixinBot
6
+ module Generators
7
+ # `rails generate mixin_bot:install` — one-command setup: runs every
8
+ # component generator in dependency order. Skip any of them:
9
+ #
10
+ # rails g mixin_bot:install --skip-notifications --skip-transfers
11
+ # rails g mixin_bot:install --skip notifications transfers
12
+ # rails g mixin_bot:install --skip=notifications,transfers
13
+ #
14
+ # Idempotent: each component generator (and this one) is safe to re-run.
15
+ class InstallGenerator < Rails::Generators::Base
16
+ COMPONENTS = %w[authentication blaze outputs transfers notifications].freeze
17
+
18
+ class_option :skip,
19
+ type: :array, default: [],
20
+ banner: 'component,component2',
21
+ desc: "Skip components (comma-separated; any of: #{COMPONENTS.join(' ')})"
22
+
23
+ COMPONENTS.each do |component|
24
+ class_option :"skip_#{component}", type: :boolean, default: false,
25
+ desc: "Skip the #{component} component"
26
+
27
+ define_method(:"install_#{component}") do
28
+ invoke "mixin_bot:#{component}" unless skip?(component)
29
+ end
30
+ end
31
+
32
+ def show_plan
33
+ skipped = COMPONENTS.select { |component| skip?(component) }
34
+ say_status :install, if skipped.empty?
35
+ 'all components'
36
+ else
37
+ "all components except #{skipped.join(', ')}"
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def skip?(component)
44
+ return true if options[:"skip_#{component}"]
45
+
46
+ Array(options[:skip]).flat_map { |value| value.to_s.split(',') }.include?(component.to_s)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Adds Noticed-driven Mixin message notifications: ensures the `noticed`
3
+ gem is in the Gemfile and scaffolds one example notification delivered
4
+ through MixinBot's Mixin channel (`deliver_by :mixin`) over the HTTP
5
+ message API.
6
+
7
+ Example:
8
+ rails generate mixin_bot:notifications
9
+
10
+ This will create:
11
+ app/notifications/payment_received_notification.rb
12
+
13
+ It will also ensure `noticed` is present in the Gemfile.