monkrb 0.20.0 → 0.21.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +7 -6
  4. data/exe/monk +3 -181
  5. data/lib/monk/assets.rb +39 -9
  6. data/lib/monk/auth/errors.rb +3 -0
  7. data/lib/monk/auth/helpers.rb +11 -0
  8. data/lib/monk/auth/send_login_link.rb +36 -0
  9. data/lib/monk/auth.rb +34 -5
  10. data/lib/monk/bin.rb +107 -0
  11. data/lib/monk/cli.rb +277 -0
  12. data/lib/monk/context.rb +17 -0
  13. data/lib/monk/errors.rb +0 -6
  14. data/lib/monk/generator/definition.rb +153 -0
  15. data/lib/monk/generator/result.rb +279 -0
  16. data/lib/monk/generator/run.rb +449 -0
  17. data/lib/monk/generator.rb +48 -0
  18. data/lib/monk/generators/auth.rb +29 -0
  19. data/lib/monk/generators/base.rb +19 -0
  20. data/lib/monk/generators/jobs.rb +21 -0
  21. data/lib/monk/generators/live.rb +26 -0
  22. data/lib/monk/generators/mail.rb +24 -0
  23. data/lib/monk/generators/postgres.rb +23 -0
  24. data/lib/monk/generators/redis.rb +20 -0
  25. data/lib/monk/generators/websocket.rb +24 -0
  26. data/lib/monk/generators.rb +7 -0
  27. data/lib/monk/jobs.rb +7 -0
  28. data/lib/monk/live/helpers.rb +13 -0
  29. data/lib/monk/live.rb +12 -1
  30. data/lib/monk/log.rb +10 -1
  31. data/lib/monk/mail/deliver_later.rb +54 -0
  32. data/lib/monk/mail/later.rb +3 -51
  33. data/lib/monk/mail.rb +4 -0
  34. data/lib/monk/templates/auth/agents.md +29 -0
  35. data/lib/monk/templates/auth/app/mailers/{app_mailer.rb → auth_mailer.rb} +5 -5
  36. data/lib/monk/templates/auth/app/routes/auth.rb +48 -0
  37. data/lib/monk/templates/auth/app/views/auth/login.erb +25 -0
  38. data/lib/monk/templates/auth/config/auth.rb +8 -8
  39. data/lib/monk/templates/auth/setup.md +37 -0
  40. data/lib/monk/templates/auth/test/auth_test.rb +17 -0
  41. data/lib/monk/templates/base/CLAUDE.md +1 -0
  42. data/lib/monk/templates/base/Dockerfile +4 -3
  43. data/lib/monk/templates/base/Gemfile +7 -2
  44. data/lib/monk/templates/base/Rakefile +8 -0
  45. data/lib/monk/templates/base/agents.md +41 -0
  46. data/lib/monk/templates/base/app/app.rb +5 -0
  47. data/lib/monk/templates/base/app/routes/.keep +0 -0
  48. data/lib/monk/templates/base/app/views/index.erb +1 -1
  49. data/lib/monk/templates/base/app/views/layouts/app.erb +2 -0
  50. data/lib/monk/templates/base/config/load.rb +8 -0
  51. data/lib/monk/templates/base/config/settings.rb +4 -5
  52. data/lib/monk/templates/base/setup.md +22 -0
  53. data/lib/monk/templates/base/test/app_test.rb +12 -0
  54. data/lib/monk/templates/base/test/test_helper.rb +18 -0
  55. data/lib/monk/templates/jobs/agents.md +25 -0
  56. data/lib/monk/templates/jobs/app/routes/jobs.rb +9 -0
  57. data/lib/monk/templates/jobs/bin/jobs +19 -13
  58. data/lib/monk/templates/jobs/setup.md +30 -0
  59. data/lib/monk/templates/jobs/test/jobs_test.rb +16 -0
  60. data/lib/monk/templates/live/agents.md +25 -0
  61. data/lib/monk/templates/live/app/broadcasts/greeting.rb +13 -0
  62. data/lib/monk/templates/live/app/routes/demo_live.rb +21 -0
  63. data/lib/monk/templates/live/app/routes/live.rb +10 -0
  64. data/lib/monk/templates/live/app/views/demo/live.erb +18 -0
  65. data/lib/monk/templates/live/app/views/live/_greeting.erb +1 -0
  66. data/lib/monk/templates/live/config/live.rb +30 -32
  67. data/lib/monk/templates/live/config/live_without_demo.rb +38 -0
  68. data/lib/monk/templates/live/setup.md +39 -0
  69. data/lib/monk/templates/live/test/live_test.rb +22 -0
  70. data/lib/monk/templates/mail/agents.md +22 -0
  71. data/lib/monk/templates/mail/app/mailers/app_mailer.rb +14 -0
  72. data/lib/monk/templates/mail/app/routes/mail.rb +12 -0
  73. data/lib/monk/templates/mail/app/views/mail/welcome.erb +1 -0
  74. data/lib/monk/templates/mail/config/mail.rb +4 -4
  75. data/lib/monk/templates/mail/setup.md +32 -0
  76. data/lib/monk/templates/mail/test/mail_test.rb +13 -0
  77. data/lib/monk/templates/postgres/agents.md +25 -0
  78. data/lib/monk/templates/postgres/app/routes/postgres.rb +22 -0
  79. data/lib/monk/templates/postgres/bin/console +3 -1
  80. data/lib/monk/templates/postgres/bin/migrate +23 -17
  81. data/lib/monk/templates/postgres/bin/setup_db +11 -5
  82. data/lib/monk/templates/postgres/config/persistence.rb +1 -0
  83. data/lib/monk/templates/postgres/setup.md +38 -0
  84. data/lib/monk/templates/postgres/test/persistence_test.rb +11 -0
  85. data/lib/monk/templates/redis/agents.md +18 -0
  86. data/lib/monk/templates/redis/app/routes/redis.rb +15 -0
  87. data/lib/monk/templates/redis/config/redis.rb +13 -0
  88. data/lib/monk/templates/redis/setup.md +20 -0
  89. data/lib/monk/templates/redis/test/redis_test.rb +12 -0
  90. data/lib/monk/templates/websocket/agents.md +25 -0
  91. data/lib/monk/templates/websocket/app/sockets/chat.rb +18 -0
  92. data/lib/monk/templates/websocket/bin/websocket_server +58 -0
  93. data/lib/monk/templates/websocket/config/websocket_postgres.rb +24 -0
  94. data/lib/monk/templates/websocket/config/websocket_redis.rb +21 -0
  95. data/lib/monk/templates/websocket/setup.md +51 -0
  96. data/lib/monk/templates/websocket/test/websocket_test.rb +21 -0
  97. data/lib/monk/version.rb +1 -1
  98. data/lib/monk/websocket/server.rb +3 -1
  99. metadata +81 -14
  100. data/lib/monk/scaffold.rb +0 -1063
  101. data/lib/monk/templates/base/bin/websocket_server +0 -77
  102. data/lib/monk/templates/jobs/app/jobs/send_login_link.rb +0 -28
  103. data/lib/monk/templates/live/app/app.rb +0 -22
  104. data/lib/monk/templates/live/app/views/index.erb +0 -18
  105. data/lib/monk/templates/live/bin/websocket_server +0 -50
  106. data/lib/monk/templates/live/config/live_pg.rb +0 -60
  107. data/lib/monk/templates/mail/Gemfile.extra +0 -1
  108. data/lib/monk/templates/postgres/Dockerfile +0 -32
  109. data/lib/monk/templates/postgres/Gemfile.extra +0 -2
  110. data/lib/monk/templates/redis/Gemfile.extra +0 -1
  111. /data/lib/monk/templates/live/app/views/{live → demo}/_hits.erb +0 -0
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require "bundler/setup"
3
- require_relative "../config/settings"
4
-
5
- # config/auth.rb only exists if this app was scaffolded with --auth -- a
6
- # missing file is a harmless no-op, the same way config/settings.rb treats
7
- # a missing dotenv gem. Present or not, it's what decides whether this
8
- # process authenticates connections below.
9
- begin
10
- require_relative "../config/auth"
11
- rescue LoadError
12
- end
13
-
14
- require "monk/websocket"
15
-
16
- # A single broadcast chat channel: every connection subscribes to :chat,
17
- # and whatever one connection sends is broadcast to everyone on it
18
- # (including itself), prefixed with the authenticated subject when there
19
- # is one.
20
- #
21
- # Defined inside a module, not at this script's own top level -- self at a
22
- # script's top level is the main object, which is *not* Ractor-shareable,
23
- # so a proc built there fails Server#run's Ractor.make_shareable check.
24
- # Module bodies are always shareable, same as StateRactor#update's block
25
- # requirement.
26
- module ChatServer
27
- # REDIS_URL set (by an app scaffolded with --redis, or by hand) wraps the
28
- # registry so :chat also fans out to every other Monk::WebSocket::Server
29
- # process sharing that Redis -- unset, it's in-process only. Either way
30
- # the app code above never has to know which: RedisFanout has the exact
31
- # same #register/#unregister/#count/#broadcast interface as Registry.
32
- REGISTRY =
33
- if (redis_url = ENV["REDIS_URL"])
34
- require "monk/websocket/redis_fanout"
35
- Monk::WebSocket::RedisFanout.new(Monk::WebSocket::Registry.new, redis_url: redis_url)
36
- else
37
- Monk::WebSocket::Registry.new
38
- end
39
-
40
- HANDLER = proc do |connection|
41
- connection.subscribe(REGISTRY, :chat)
42
- loop do
43
- message = connection.read
44
- break unless message
45
-
46
- REGISTRY.broadcast(:chat, "#{connection.subject || "guest"}: #{message}")
47
- end
48
- end
49
- end
50
-
51
- # This process holds the sockets, so it's the one that listens for other
52
- # processes' broadcasts: with REDIS_URL, a subscriber connection, opened
53
- # here at boot (an unreachable Redis fails right here). A plain Registry
54
- # has nothing to listen to, and listen! does nothing.
55
- ChatServer::REGISTRY.listen!
56
-
57
- # false, not nil, without --auth: Server.new refuses nil.
58
- authenticate = !!(defined?(Monk::Auth) && Monk::Auth.config)
59
- # Sessions are checked through config/auth.rb's :auth pool rather than in
60
- # each socket's own Ractor, which would hold a Postgres connection per
61
- # open page. This is the only process that uses the pool, so it starts it.
62
- db_pool = (:auth if authenticate)
63
- Monk::Persistence::Pg.start_pools!(db_pool) if db_pool
64
-
65
- port = ENV.fetch("WS_PORT", "9293").to_i
66
- # Defaults to public_url (config/settings.rb) so one PUBLIC_URL env var
67
- # keeps this in sync with the app's actual origin instead of two vars
68
- # drifting apart; set WS_ALLOWED_ORIGINS directly for anything more, e.g.
69
- # more than one origin (comma-separated).
70
- allowed_origins = ENV.fetch("WS_ALLOWED_ORIGINS", Monk::Settings[:public_url]).split(",")
71
-
72
- server = Monk::WebSocket::Server.new(
73
- port: port, authenticate: authenticate, allowed_origins: allowed_origins, db_pool: db_pool,
74
- )
75
- puts "Monk::WebSocket::Server listening on ws://0.0.0.0:#{port} " \
76
- "(authenticate: #{authenticate}, redis fan-out: #{ENV["REDIS_URL"] ? "on" : "off"})"
77
- server.run(&ChatServer::HANDLER)
@@ -1,28 +0,0 @@
1
- # Sends a magic link from bin/jobs instead of the login request. The token
2
- # is created here, right before it's sent, so the raw token is never
3
- # stored anywhere -- not even in the job queue, which a plain
4
- # Monk::Mail.deliver_later of the link would do (Monk's
5
- # docs/adr/0014-mail-from-jobs-and-login-links-created-in-the-job.md).
6
- # Enqueue it from your login route, after your own per-email rate limit
7
- # and with a redirect_to already checked against redirect_allowlist:
8
- #
9
- # post("/auth/request") do
10
- # SendLoginLink.enqueue(params[:email])
11
- # json(sent: true)
12
- # end
13
- class SendLoginLink < Monk::Job
14
- queue "mailers"
15
- # About 50 seconds of retries: a login email minutes late, after the
16
- # user has likely asked for another, is worse than none.
17
- max_attempts 3
18
- never_retry Monk::InvalidRedirectError
19
-
20
- def self.perform(email, redirect_to = nil)
21
- token = Monk::Auth.request_login(email, redirect_to: redirect_to)
22
- # Your callback route; config/settings.rb's public_url is the origin.
23
- link = "#{Monk::Settings[:public_url]}/auth/callback/#{token}"
24
- # Calls config/auth.rb's deliver: (AppMailer::MAGIC_LINK) -- a
25
- # synchronous send, here in the job.
26
- Monk::Auth.deliver_link(email: email, link: link, token: token)
27
- end
28
- end
@@ -1,22 +0,0 @@
1
- class App < Monk::Base
2
- views "app/views"
3
- layout "layouts/app"
4
- assets "public"
5
-
6
- # State shared across requests lives in a StateRactor; the update block is
7
- # built here, where self is shareable, not inline in the route.
8
- hits = Monk::StateRactor.new(0)
9
- increment = Ractor.make_shareable(proc { |n| n + 1 })
10
-
11
- get("/") { @title = "App"; render "index", hits: hits.value }
12
-
13
- # Change state, then tell every open page that shows it. Monk::Live.patch
14
- # renders the partial once and pushes it to the "hits" topic's subscribers.
15
- post("/hit") do
16
- Monk::Live.patch("hits", to: "#hits", partial: "live/_hits", hits: hits.update(&increment))
17
- redirect "/"
18
- end
19
-
20
- get("/hello") { "hello from monk" }
21
- get("/api/hello") { json(message: "hello from monk") }
22
- end
@@ -1,18 +0,0 @@
1
- <h1>Monk::Live</h1>
2
- <p>
3
- Open this page in two tabs and press the button in one: the counter in the
4
- other updates by itself. The server pushes the new HTML over a WebSocket;
5
- there is no polling and no page JavaScript of your own.
6
- </p>
7
-
8
- <%# live_topic makes this element's page subscribe to the "hits" topic. %>
9
- <p <%= live_topic "hits" %>>Hits so far: <%= render "live/_hits", hits: locals[:hits], layout: false %></p>
10
-
11
- <form method="post" action="/hit"><button>+1</button></form>
12
-
13
- <p>
14
- The pieces: <code>live_topic</code> in this view, the partial
15
- <code>app/views/live/_hits.erb</code>, <code>Monk::Live.patch</code> in
16
- <code>POST /hit</code> (<code>app/app.rb</code>), and the rules and Redis
17
- wiring in <code>config/live.rb</code>.
18
- </p>
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require "bundler/setup"
3
- require_relative "../config/settings"
4
-
5
- # config/auth.rb only exists if this app was scaffolded with --auth -- a
6
- # missing file is a harmless no-op, the same way config/settings.rb treats
7
- # a missing dotenv gem. Present or not, it's what decides whether this
8
- # process authenticates connections below.
9
- begin
10
- require_relative "../config/auth"
11
- rescue LoadError
12
- end
13
-
14
- # Monk::Live: the browsers' sockets end here. The handler takes their
15
- # subscribe/unsubscribe messages, checks each topic against the rules in
16
- # config/live.rb, and relays whatever bin/server's process publishes over
17
- # Redis.
18
- require_relative "../config/live"
19
-
20
- # This process holds the browsers' sockets, so it's the only one that
21
- # listens for updates published elsewhere (bin/server, bin/jobs): the
22
- # fanout's subscriber connection is opened here, at boot, and an
23
- # unreachable Redis/Postgres fails right here.
24
- Monk::Live.listen!
25
-
26
- # With Monk::Auth (--auth), a socket with a valid session gets its user,
27
- # and one without -- a visitor, or a session since revoked -- comes in
28
- # anonymous instead of refused. The rules in config/live.rb decide what
29
- # each may subscribe to: a topic is open to visitors only if its rule says
30
- # `anonymous: true`, like the demo's "hits". Without Monk::Auth every
31
- # socket is anonymous.
32
- authenticate = defined?(Monk::Auth) && Monk::Auth.config ? :optional : false
33
- # Sessions are checked through config/auth.rb's :auth pool rather than in
34
- # each socket's own Ractor, which would hold a Postgres connection per
35
- # open page. This is the only process that uses the pool, so it starts it.
36
- db_pool = (:auth if authenticate)
37
- Monk::Persistence::Pg.start_pools!(db_pool) if db_pool
38
-
39
- port = ENV.fetch("WS_PORT", "9293").to_i
40
- # Defaults to public_url (config/settings.rb) so one PUBLIC_URL env var
41
- # keeps this in sync with the app's actual origin instead of two vars
42
- # drifting apart; set WS_ALLOWED_ORIGINS directly for anything more, e.g.
43
- # more than one origin (comma-separated).
44
- allowed_origins = ENV.fetch("WS_ALLOWED_ORIGINS", Monk::Settings[:public_url]).split(",")
45
-
46
- server = Monk::WebSocket::Server.new(
47
- port: port, authenticate: authenticate, allowed_origins: allowed_origins, db_pool: db_pool,
48
- )
49
- puts "Monk::Live WebSocket server listening on ws://0.0.0.0:#{port} (authenticate: #{authenticate})"
50
- server.run(&Monk::Live::HANDLER)
@@ -1,60 +0,0 @@
1
- # Monk::Live wiring, required by both processes: config/load.rb (this app
2
- # publishes updates, e.g. from a route) and bin/websocket_server (the browsers'
3
- # sockets terminate there and are handed the updates).
4
- #
5
- # They are separate processes, so *something* has to carry a publish from
6
- # one to the other. `monk new --live --postgres` (and not --redis) wired
7
- # this file instead of the Redis-based default -- Postgres LISTEN/NOTIFY
8
- # carries the publish instead, reusing the exact DB_* env vars
9
- # config/persistence.rb already reads, so there's no Redis to run. The
10
- # tradeoff: a single broadcast is capped at just under 8000 bytes
11
- # (Monk::WebSocket::PgFanout::MAX_NOTIFY_PAYLOAD_BYTES, Postgres's own
12
- # NOTIFY limit) -- regenerate with --redis instead if that becomes a real
13
- # constraint. Full design in the monk gem's own docs/design/live-pg-fanout.md.
14
- require_relative "settings"
15
- require "monk/live"
16
- require "monk/websocket/pg_fanout"
17
-
18
- # Where the browser opens its WebSocket (read by the layout). In
19
- # development there's no reverse proxy in front, so this is the direct WS
20
- # port; outside development it defaults to a /ws path under public_url
21
- # (config/settings.rb) -- matching the path-based proxy routing
22
- # docs/guides/deploying.md's "Adding Monk::WebSocket" section sets up, so
23
- # setting PUBLIC_URL alone keeps this in sync instead of two env vars
24
- # drifting apart. Override with LIVE_WS_URL directly if your setup doesn't
25
- # fit that shape.
26
- default_live_ws_url =
27
- if Monk.env.development?
28
- "ws://localhost:9293"
29
- else
30
- Monk::Settings[:public_url].sub(/\Ahttps:\/\//, "wss://").sub(/\Ahttp:\/\//, "ws://") + "/ws"
31
- end
32
-
33
- Monk::Settings.configure do
34
- optional :live_ws_url, default: default_live_ws_url
35
- end
36
-
37
- Monk::Live.configure(
38
- registry: Monk::WebSocket::PgFanout.new(
39
- Monk::WebSocket::Registry.new,
40
- pg_opts: {
41
- host: ENV.fetch("DB_HOST", "127.0.0.1"),
42
- port: ENV.fetch("DB_PORT", "5432").to_i,
43
- user: ENV.fetch("DB_USER", "postgres"),
44
- password: ENV.fetch("DB_PASSWORD", "postgres"),
45
- dbname: ENV.fetch("DB_NAME", "app_development"),
46
- },
47
- ),
48
- )
49
-
50
- # Who may subscribe to what. Nothing is allowed unless a rule says so, and an
51
- # anonymous connection is denied unless the rule opts in with `anonymous: true`.
52
- # The rule blocks live in a module because they have to be Ractor-shareable
53
- # (self at the top of this file is not).
54
- module AppLive
55
- # The demo counter is public. A real rule looks at who is asking:
56
- # proc { |subject, topic| topic == "contacts:#{subject}" }
57
- PUBLIC = proc { |_subject, _topic| true }
58
- end
59
-
60
- Monk::Live.authorize("hits", anonymous: true, &AppLive::PUBLIC)
@@ -1 +0,0 @@
1
- gem "net-smtp" # Monk::Mail's smtp:// transport (config/mail.rb)
@@ -1,32 +0,0 @@
1
- # Same shape as the base Dockerfile, plus libpq: the pg gem's native
2
- # extension needs libpq-dev (headers) to compile in the builder stage and
3
- # libpq5 (the runtime lib alone, no headers) in the final image.
4
- FROM ruby:4.0-slim AS builder
5
- WORKDIR /app
6
-
7
- RUN apt-get update -qq \
8
- && apt-get install -y --no-install-recommends build-essential libpq-dev \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- COPY Gemfile Gemfile.lock* ./
12
- RUN bundle install
13
-
14
- FROM ruby:4.0-slim
15
- WORKDIR /app
16
-
17
- RUN apt-get update -qq \
18
- && apt-get install -y --no-install-recommends libpq5 \
19
- && rm -rf /var/lib/apt/lists/*
20
-
21
- COPY --from=builder /usr/local/bundle /usr/local/bundle
22
- COPY . .
23
-
24
- # Matches bin/server's own default (PORT overrides both, e.g. on a
25
- # platform that assigns its own). bin/websocket_server (WS_PORT default
26
- # 9293) is a separate process/deploy unit -- run this same image with
27
- # `bin/websocket_server` as the command instead of publishing another
28
- # port from this Dockerfile -- see docs/guides/deploying.md. With --jobs,
29
- # bin/jobs is another one, the same way: this image, `bin/jobs` as the
30
- # command, no port at all.
31
- EXPOSE 9292
32
- CMD ["bin/server", "--bind", "0.0.0.0"]
@@ -1,2 +0,0 @@
1
- gem "pg"
2
- gem "irb"
@@ -1 +0,0 @@
1
- gem "redis"