monkrb 0.20.0 → 0.21.1
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 +4 -4
- data/CHANGELOG.md +99 -0
- data/README.md +7 -6
- data/exe/monk +3 -181
- data/lib/monk/assets.rb +39 -9
- data/lib/monk/auth/errors.rb +3 -0
- data/lib/monk/auth/helpers.rb +11 -0
- data/lib/monk/auth/send_login_link.rb +36 -0
- data/lib/monk/auth.rb +34 -5
- data/lib/monk/bin.rb +107 -0
- data/lib/monk/cli.rb +277 -0
- data/lib/monk/context.rb +17 -0
- data/lib/monk/errors.rb +0 -6
- data/lib/monk/generator/definition.rb +153 -0
- data/lib/monk/generator/result.rb +279 -0
- data/lib/monk/generator/run.rb +449 -0
- data/lib/monk/generator.rb +48 -0
- data/lib/monk/generators/auth.rb +29 -0
- data/lib/monk/generators/base.rb +19 -0
- data/lib/monk/generators/jobs.rb +21 -0
- data/lib/monk/generators/live.rb +26 -0
- data/lib/monk/generators/mail.rb +24 -0
- data/lib/monk/generators/postgres.rb +23 -0
- data/lib/monk/generators/redis.rb +20 -0
- data/lib/monk/generators/websocket.rb +24 -0
- data/lib/monk/generators.rb +7 -0
- data/lib/monk/jobs.rb +7 -0
- data/lib/monk/live/helpers.rb +13 -0
- data/lib/monk/live.rb +12 -1
- data/lib/monk/log.rb +10 -1
- data/lib/monk/mail/deliver_later.rb +54 -0
- data/lib/monk/mail/later.rb +3 -51
- data/lib/monk/mail.rb +4 -0
- data/lib/monk/templates/auth/agents.md +29 -0
- data/lib/monk/templates/auth/app/mailers/{app_mailer.rb → auth_mailer.rb} +5 -5
- data/lib/monk/templates/auth/app/routes/auth.rb +48 -0
- data/lib/monk/templates/auth/app/views/auth/login.erb +25 -0
- data/lib/monk/templates/auth/config/auth.rb +8 -8
- data/lib/monk/templates/auth/setup.md +37 -0
- data/lib/monk/templates/auth/test/auth_test.rb +17 -0
- data/lib/monk/templates/base/CLAUDE.md +1 -0
- data/lib/monk/templates/base/Dockerfile +10 -5
- data/lib/monk/templates/base/Gemfile +7 -2
- data/lib/monk/templates/base/Rakefile +8 -0
- data/lib/monk/templates/base/agents.md +41 -0
- data/lib/monk/templates/base/app/app.rb +5 -0
- data/lib/monk/templates/base/app/routes/.keep +0 -0
- data/lib/monk/templates/base/app/views/index.erb +1 -1
- data/lib/monk/templates/base/app/views/layouts/app.erb +2 -0
- data/lib/monk/templates/base/config/load.rb +14 -0
- data/lib/monk/templates/base/config/settings.rb +4 -5
- data/lib/monk/templates/base/setup.md +22 -0
- data/lib/monk/templates/base/test/app_test.rb +12 -0
- data/lib/monk/templates/base/test/test_helper.rb +18 -0
- data/lib/monk/templates/jobs/agents.md +25 -0
- data/lib/monk/templates/jobs/app/routes/jobs.rb +9 -0
- data/lib/monk/templates/jobs/bin/jobs +19 -13
- data/lib/monk/templates/jobs/setup.md +30 -0
- data/lib/monk/templates/jobs/test/jobs_test.rb +16 -0
- data/lib/monk/templates/live/agents.md +25 -0
- data/lib/monk/templates/live/app/broadcasts/greeting.rb +13 -0
- data/lib/monk/templates/live/app/routes/demo_live.rb +21 -0
- data/lib/monk/templates/live/app/routes/live.rb +10 -0
- data/lib/monk/templates/live/app/views/demo/live.erb +18 -0
- data/lib/monk/templates/live/app/views/live/_greeting.erb +1 -0
- data/lib/monk/templates/live/config/live.rb +30 -32
- data/lib/monk/templates/live/config/live_without_demo.rb +38 -0
- data/lib/monk/templates/live/setup.md +39 -0
- data/lib/monk/templates/live/test/live_test.rb +22 -0
- data/lib/monk/templates/mail/agents.md +22 -0
- data/lib/monk/templates/mail/app/mailers/app_mailer.rb +14 -0
- data/lib/monk/templates/mail/app/routes/mail.rb +12 -0
- data/lib/monk/templates/mail/app/views/mail/welcome.erb +1 -0
- data/lib/monk/templates/mail/config/mail.rb +4 -4
- data/lib/monk/templates/mail/setup.md +32 -0
- data/lib/monk/templates/mail/test/mail_test.rb +13 -0
- data/lib/monk/templates/postgres/agents.md +25 -0
- data/lib/monk/templates/postgres/app/routes/postgres.rb +22 -0
- data/lib/monk/templates/postgres/bin/console +3 -1
- data/lib/monk/templates/postgres/bin/migrate +23 -17
- data/lib/monk/templates/postgres/bin/setup_db +11 -5
- data/lib/monk/templates/postgres/config/persistence.rb +1 -0
- data/lib/monk/templates/postgres/setup.md +38 -0
- data/lib/monk/templates/postgres/test/persistence_test.rb +11 -0
- data/lib/monk/templates/redis/agents.md +18 -0
- data/lib/monk/templates/redis/app/routes/redis.rb +15 -0
- data/lib/monk/templates/redis/config/redis.rb +13 -0
- data/lib/monk/templates/redis/setup.md +20 -0
- data/lib/monk/templates/redis/test/redis_test.rb +12 -0
- data/lib/monk/templates/websocket/agents.md +28 -0
- data/lib/monk/templates/websocket/app/sockets/chat.rb +18 -0
- data/lib/monk/templates/websocket/bin/websocket_server +62 -0
- data/lib/monk/templates/websocket/config/websocket_postgres.rb +38 -0
- data/lib/monk/templates/websocket/config/websocket_redis.rb +26 -0
- data/lib/monk/templates/websocket/setup.md +54 -0
- data/lib/monk/templates/websocket/test/websocket_test.rb +21 -0
- data/lib/monk/version.rb +1 -1
- data/lib/monk/websocket/server.rb +3 -1
- metadata +81 -14
- data/lib/monk/scaffold.rb +0 -1063
- data/lib/monk/templates/base/bin/websocket_server +0 -77
- data/lib/monk/templates/jobs/app/jobs/send_login_link.rb +0 -28
- data/lib/monk/templates/live/app/app.rb +0 -22
- data/lib/monk/templates/live/app/views/index.erb +0 -18
- data/lib/monk/templates/live/bin/websocket_server +0 -50
- data/lib/monk/templates/live/config/live_pg.rb +0 -60
- data/lib/monk/templates/mail/Gemfile.extra +0 -1
- data/lib/monk/templates/postgres/Dockerfile +0 -32
- data/lib/monk/templates/postgres/Gemfile.extra +0 -2
- data/lib/monk/templates/redis/Gemfile.extra +0 -1
- /data/lib/monk/templates/live/app/views/{live → demo}/_hits.erb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd1a1140ab4c223d2bdd9c10a226ea274e0051af1ab01444194cb4dc88ebb166
|
|
4
|
+
data.tar.gz: 9fab9365ba1a31224caf14038c2e36c0ba59c9cc70dd60c037f4dbfa78aec8c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36bf94be6268d9f12fbb703132f96b00a84c52e5eeb6b03d9b90b9e981d5c9be2ee89003ecc7952b2880d288454d4d6d1734ffb6cfd8bd70328dded0cbbc1e0e
|
|
7
|
+
data.tar.gz: 58c8a897231b31edcc82cfd0e3162c64633a2a121f2b3cc01dbbe3b2fa6ef7a7d9760eb2b623a4b6b8def85004322abba20b001758dadd291f8a2ada52b3d26b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,105 @@ All notable changes to this project are documented here. Format is loosely
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); versions are as released
|
|
5
5
|
in `lib/monk/version.rb`.
|
|
6
6
|
|
|
7
|
+
## 0.21.1 - 2026-10-09
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`monk add websocket --transport=postgres` publishes through the
|
|
12
|
+
`:notify` pool.** `config/websocket.rb` declares
|
|
13
|
+
`Monk::Persistence::Pg.pool(:notify, size: 1)` and builds
|
|
14
|
+
`PgFanout.new(..., db_pool: :notify)`, so a process holds one publishing
|
|
15
|
+
connection however many Ractors publish. Before, every `bin/server`
|
|
16
|
+
worker opened its own. `AppWebSocket.start!` starts the pool (with
|
|
17
|
+
Redis it does nothing): `config/load.rb` calls it at the end, which
|
|
18
|
+
covers `bin/server`, `bin/jobs`, `bin/console` and the tests, and
|
|
19
|
+
`bin/websocket_server` calls it too. An existing app: replace
|
|
20
|
+
`config/websocket.rb` with
|
|
21
|
+
`monk help websocket --file config/websocket.rb --transport=postgres`
|
|
22
|
+
and add the last lines of `monk help base --file config/load.rb`.
|
|
23
|
+
- **The scaffolded `Dockerfile` builds with `monk add postgres`.** Its
|
|
24
|
+
builder stage installs `build-essential`: `irb`'s dependencies (`erb`,
|
|
25
|
+
`io-console`, `prism`) compile native extensions. The image that runs
|
|
26
|
+
still has no compiler. An existing app: add the `apt-get` line from
|
|
27
|
+
`monk help base --file Dockerfile`.
|
|
28
|
+
|
|
29
|
+
## 0.21.0 - 2026-10-09
|
|
30
|
+
|
|
31
|
+
### Changed (breaking)
|
|
32
|
+
|
|
33
|
+
- **`monk new` no longer takes module flags.** `--postgres`, `--auth`,
|
|
34
|
+
`--mail`, `--jobs`, `--redis` and `--live` are a usage error that shows
|
|
35
|
+
the same command's new form, and nothing is written. Modules are added
|
|
36
|
+
with `monk add MODULE`, on any app, new or existing, or with
|
|
37
|
+
`monk new NAME --with a,b` (ADR 0017, `docs/guides/scaffolding.md`).
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`monk add`**: one generator per module (`postgres`, `redis`, `mail`,
|
|
42
|
+
`auth`, `jobs`, `websocket`, `live`). Each adds its dependencies first,
|
|
43
|
+
creates files and appends lines (never edits or replaces one), and
|
|
44
|
+
brings its wiring, commented examples of how to use it (tagged
|
|
45
|
+
`# monk:example`), a test in `test/`, and its sections of `SETUP.md` and
|
|
46
|
+
`AGENTS.md`. Adding a module twice does nothing. A file the module needs
|
|
47
|
+
that the app already has with other content stops the command before
|
|
48
|
+
anything is written; any other is left alone. `monk add --list`,
|
|
49
|
+
`monk help MODULE [--file PATH]`, `--dry-run`, and `--json` for agents
|
|
50
|
+
and scripts, with exit codes 0 (done), 1 (usage), 2 (a missing choice)
|
|
51
|
+
and 3 (a conflict). A missing choice is asked in a terminal, never
|
|
52
|
+
elsewhere.
|
|
53
|
+
- **Redis and WebSocket are modules of their own.** `monk add redis` is for
|
|
54
|
+
any use of Redis (a cache, rate limits). `monk add websocket
|
|
55
|
+
--transport=postgres|redis` writes `config/websocket.rb`, one registry
|
|
56
|
+
(`AppWebSocket::REGISTRY`) over the chosen transport, and the new
|
|
57
|
+
`bin/websocket_server`; the transport is inferred when only one is
|
|
58
|
+
installed. Live uses websocket's registry.
|
|
59
|
+
- **Every app ships its tests** (`Rakefile`, `test/test_helper.rb`,
|
|
60
|
+
`test/app_test.rb`), an `AGENTS.md` for coding agents and a `CLAUDE.md`
|
|
61
|
+
importing it; dotenv is in every `Gemfile`.
|
|
62
|
+
- **Module routes live in `app/routes/`**, which `app/app.rb` loads; each
|
|
63
|
+
file reopens `class App` (amends ADR 0015). `config/load.rb` is the same
|
|
64
|
+
for every app: it requires each module's config that exists, in a fixed
|
|
65
|
+
order.
|
|
66
|
+
- `Monk::Auth.configure(callback_path:)` (default `"/auth/callback"`) and
|
|
67
|
+
`Monk::Auth.login_link(token)`. `Monk::Auth::SendLoginLink`, the job that
|
|
68
|
+
creates and sends a login link, ships with Monk, once auth and jobs are
|
|
69
|
+
both loaded. `log_out!` ends the current session and clears its cookies.
|
|
70
|
+
- `Monk::Mail.deliver_later` is defined once `monk/mail` and `monk/jobs`
|
|
71
|
+
are both loaded, in either order; `require "monk/mail/later"` still
|
|
72
|
+
works.
|
|
73
|
+
- `<%= monk_head %>` in a layout renders the `<head>` tags of the loaded
|
|
74
|
+
modules (Live's, once `config/live.rb` has configured it).
|
|
75
|
+
- `Monk::Assets.mount(prefix, dir)` serves a module's files from the gem
|
|
76
|
+
under `/_monk/`; Monk::Live's browser client is served at
|
|
77
|
+
`/_monk/live/`, so apps no longer copy it.
|
|
78
|
+
- `Monk::Live::ALLOW_ALL`, a shareable rule block letting everyone in.
|
|
79
|
+
- `Monk::Bin.run(__FILE__) { ... }`: a scaffolded app's `bin/` scripts end
|
|
80
|
+
with what failed and the command that fixes it (Postgres or Redis not
|
|
81
|
+
running, a missing database, refused credentials, an unset setting),
|
|
82
|
+
exit 1, instead of a backtrace.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- `monk new`'s `Gemfile` asks for `gem "monkrb", "~> 0.21"`: a new app
|
|
87
|
+
needs this version's APIs (`monk_head`, `log_out!`, `Monk::Bin`), so an
|
|
88
|
+
older monkrb installed on the machine can't satisfy it.
|
|
89
|
+
- `JOBS_QUEUES` defaults to `mailers,default` in `bin/jobs`, with or
|
|
90
|
+
without mail.
|
|
91
|
+
- `monk add postgres` adds `gem "pg", "~> 1.6"`: 1.6+ installs precompiled
|
|
92
|
+
with its own libpq, so the one `Dockerfile` needs no system packages.
|
|
93
|
+
- `config/persistence.rb` loads `Monk::Persistence::Pg::Model`, so an app
|
|
94
|
+
with only Postgres can define models.
|
|
95
|
+
- Live's demo is its own files, at `/demo/live`, in development only
|
|
96
|
+
(`--no-demo` to leave it out); it no longer replaces `app/app.rb` and the
|
|
97
|
+
home page.
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- `Monk::Log` works before Boot. In `bin/console`, which never boots,
|
|
102
|
+
anything that logged raised on a nil path: `Monk::Mail.deliver` through
|
|
103
|
+
`log://`, a job's `perform`, `Monk::Log.info`. Before Boot the first
|
|
104
|
+
write now works out the same `log/<env>.log` path.
|
|
105
|
+
|
|
7
106
|
## 0.20.0 - 2026-10-05
|
|
8
107
|
|
|
9
108
|
### Fixed
|
data/README.md
CHANGED
|
@@ -18,13 +18,14 @@ gem install monkrb
|
|
|
18
18
|
monk new my_app && cd my_app
|
|
19
19
|
bundle install
|
|
20
20
|
bin/server # -> http://localhost:9292/hello
|
|
21
|
+
monk add --list # the modules you can add: postgres, auth, jobs, live...
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
`monk new` scaffolds the app's own `Gemfile` with `gem "monkrb", require: "monk"`.
|
|
24
|
+
`monk new` scaffolds the app's own `Gemfile` with `gem "monkrb", "~> 0.21", require: "monk"`.
|
|
24
25
|
|
|
25
|
-
`monk new` writes a working skeleton (an HTML home page, a `/hello` route, a `/api/hello` JSON route, `public/`, a `SETUP.md`), with the app's own code under `app/`: `app/app.rb` for routes, `app/views/`, and one directory per role (`models/`, `presenters/`, `helpers/`, `mailers/`, `broadcasts/`, `jobs/`).
|
|
26
|
+
`monk new` writes a working skeleton (an HTML home page, a `/hello` route, a `/api/hello` JSON route, `public/`, its tests, a `SETUP.md` and an `AGENTS.md` for coding agents), with the app's own code under `app/`: `app/app.rb` and `app/routes/` for routes, `app/views/`, and one directory per role (`models/`, `presenters/`, `helpers/`, `mailers/`, `broadcasts/`, `jobs/`). `monk add` adds Postgres, Redis, email, auth, background jobs, WebSocket and live updates, to a new app or an old one, each with its wiring, commented examples of how to use it, and a test (`monk new my_app --with auth,jobs` does both at once): see [`docs/guides/scaffolding.md`](docs/guides/scaffolding.md), which also says where each kind of code goes.
|
|
26
27
|
|
|
27
|
-
All `monk` commands and
|
|
28
|
+
All `monk` commands and options are listed by:
|
|
28
29
|
|
|
29
30
|
```
|
|
30
31
|
monk --help
|
|
@@ -60,11 +61,11 @@ Everything beyond the core is opt-in (`require "monk"` alone loads none of it).
|
|
|
60
61
|
| Persistence | `Monk::Persistence::Pg`: raw `pg`, per-Ractor connections, hash-based `Model` | `require "monk/persistence/pg"` (+ `.../pg/model`); needs the `pg` gem | [`persistence.md`](docs/guides/persistence.md) |
|
|
61
62
|
| Migrations | plain `.sql` up/down pairs, `Migrator` | `require "monk/persistence/pg/migrator"`; needs the `pg` gem | [`migrations.md`](docs/guides/migrations.md) |
|
|
62
63
|
| Auth and sessions | `Monk::Auth`: passwordless tokens, Bearer or cookie + CSRF | `require "monk/auth"`; needs the `pg` gem and a registered Postgres connection | [`auth.md`](docs/guides/auth.md) |
|
|
63
|
-
| Email | `Monk::Mail`: text/HTML email over SMTP or a local relay, one `MAIL_URL`, sent from any worker Ractor, or from a background job with `deliver_later` | `require "monk/mail"`
|
|
64
|
+
| Email | `Monk::Mail`: text/HTML email over SMTP or a local relay, one `MAIL_URL`, sent from any worker Ractor, or from a background job with `deliver_later` | `require "monk/mail"`; `deliver_later` is there once `monk/jobs` is loaded too; SMTP needs the `net-smtp` gem | [`mail.md`](docs/guides/mail.md) |
|
|
64
65
|
| Background jobs | `Monk::Jobs`: a queue in Postgres, run by `bin/jobs` on worker Ractors; retries with backoff, scheduled jobs, enqueue inside the app's own transaction, `drain!` for tests | `require "monk/jobs"` (+ `require "monk/jobs/runtime"` in the job process); needs the `pg` gem and a registered Postgres connection | [`jobs.md`](docs/guides/jobs.md) |
|
|
65
66
|
| WebSocket | `Monk::WebSocket`: RFC 6455 server as its own process, Redis or Postgres fan-out | `require "monk/websocket"`; fan-out: `require "monk/websocket/redis_fanout"` (+ the `redis` gem) or `require "monk/websocket/pg_fanout"` (+ the `pg` gem) | [`websocket.md`](docs/guides/websocket.md) |
|
|
66
67
|
| Live updates | `Monk::Live`: server-rendered HTML patches pushed to open tabs | `require "monk/live"`; needs the WebSocket server and, across processes, Redis or Postgres | [`live.md`](docs/guides/live.md) |
|
|
67
|
-
| Scaffolding | `monk new` and
|
|
68
|
+
| Scaffolding | `monk new` and `monk add`: one generator per module, for a new app or an existing one; text or `--json` output | — (the `monk` command: `monk new NAME --with ...`, `monk add MODULE`, `monk add --list`) | [`scaffolding.md`](docs/guides/scaffolding.md) |
|
|
68
69
|
|
|
69
70
|
## More documentation
|
|
70
71
|
|
|
@@ -87,6 +88,6 @@ More (server modes, Docker) in [`docs/development.md`](docs/development.md).
|
|
|
87
88
|
|
|
88
89
|
## Status
|
|
89
90
|
|
|
90
|
-
Monk is **pre-1.0** (see `lib/monk/version.rb` and the [changelog](CHANGELOG.md)): the API may still change between minor versions. The core is described in [`docs/history/core-plan.md`](docs/history/core-plan.md).
|
|
91
|
+
Monk is **pre-1.0** (see `lib/monk/version.rb` and the [changelog](CHANGELOG.md)): the API may still change between minor versions. The core is described in [`docs/history/core-plan.md`](docs/history/core-plan.md). Features to add are listed in [`docs/roadmap.md`](docs/roadmap.md). Done so far, each with its own design doc and plan: persistence, migrations, HTML templating and static assets, auth and sessions, WebSocket with Redis fan-out, log levels, live updates (`Monk::Live`, as of 2026-09-19), deployment support (Dockerfile scaffolding, `docs/guides/deploying.md`), the RubyGems release as `monkrb` (both 2026-09-22), and `Monk::WebSocket::PgFanout`/`monk new --live --postgres` — the same cross-process fan-out over Postgres `LISTEN`/`NOTIFY` instead of Redis, for an app that doesn't want a second piece of infrastructure (`docs/design/live-pg-fanout.md`, 2026-09-24), the built-in mailer `Monk::Mail` (0.17, 2026-09-26, ADR 0012), and background jobs on Postgres, `Monk::Jobs`, with mail sent from jobs (0.18, 2026-09-29, ADRs 0013 and 0014). In 0.19 (2026-10-01): `monk new` lays the app out under `app/`, one directory per role (ADR 0015); a WebSocket fanout listens only in the process that holds the sockets (`#listen!`); `authenticate: :optional` lets visitors' sockets in anonymously, for Live's rules to decide; and the WebSocket server stops cleanly on `TERM` (`docs/design/websocket.md`). In 0.20 (2026-10-05): Monk recovers from a dropped database connection (a Postgres restart, a failover) without a process restart. `checkout` reconnects a dead connection before running its block, never retrying the block itself; both fanouts' listeners reconnect and then make every open page resync; and a route that raises logs why (`docs/history/plan-pg-reconnect.md`). Also new: connection pools for Ractors that shouldn't each hold a connection (`Monk::Persistence::Pg.pool`). The WebSocket server's session checks, Live's subscribe rules and `PgFanout`'s publisher can run through one, so open pages share a few connections instead of holding one each: 1,000 open sockets on 4 connections (`docs/guides/persistence.md`, "Pools"; `docs/history/plan-pg-pool.md`). In 0.21 (2026-10-09): scaffolding by module. `monk new` makes the core app, and `monk add` adds a module (Postgres, Redis, mail, auth, jobs, WebSocket, Live) to any app, new or existing, with its wiring, commented examples, a test, and its sections of `SETUP.md` and an `AGENTS.md` for coding agents; `--json` for agents and scripts. Redis and WebSocket become modules of their own, the WebSocket transport is chosen once per app, and `monk new`'s module flags are gone (ADR 0017, `docs/guides/scaffolding.md`, `docs/history/plan-scaffold.md`).
|
|
91
92
|
|
|
92
93
|
[Made with Love ❤️, Ruby 💎 and AI 🤖](docs/ai_usage_disclaimer.md)
|
data/exe/monk
CHANGED
|
@@ -1,183 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
|
|
2
|
+
# monk new / monk add / monk help: docs/guides/scaffolding.md.
|
|
3
|
+
require_relative "../lib/monk/cli"
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
HELP = <<~TEXT.freeze
|
|
7
|
-
#{USAGE}
|
|
8
|
-
|
|
9
|
-
Creates a new Monk project at APP_NAME from a static skeleton -- refuses
|
|
10
|
-
to run if APP_NAME already exists.
|
|
11
|
-
|
|
12
|
-
Base skeleton: Gemfile, config.ru, .ruby-version, bin/server,
|
|
13
|
-
bin/websocket_server, SETUP.md -- WebSocket support needs no external
|
|
14
|
-
service, so unlike Postgres/Redis below it isn't gated behind a flag:
|
|
15
|
-
bin/websocket_server auto-authenticates if --auth's config/auth.rb is
|
|
16
|
-
present, and fans out across processes over Redis if --redis's
|
|
17
|
-
REDIS_URL is set, adapting at boot either way. SETUP.md is generated
|
|
18
|
-
for every flag combination, walking through dev-then-test setup for
|
|
19
|
-
this exact project -- including, since monk new scaffolds no test
|
|
20
|
-
framework itself, the minimum Minitest wiring to get a working
|
|
21
|
-
`bundle exec rake test`.
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
--postgres Also scaffold Postgres persistence/migrations support:
|
|
25
|
-
config/persistence.rb, bin/console, bin/setup_db,
|
|
26
|
-
bin/migrate, db/migrate/. Also wires config.ru to
|
|
27
|
-
require it, writes .env/.env.test/.env.example
|
|
28
|
-
(DB_NAME defaulting to APP_NAME_development/_test,
|
|
29
|
-
not the generic app_development fallback baked into
|
|
30
|
-
config/persistence.rb itself), and uncomments
|
|
31
|
-
gem "dotenv" in the Gemfile so .env actually loads
|
|
32
|
-
(config/settings.rb's require "dotenv/load" is a
|
|
33
|
-
no-op with it left commented). SETUP.md's dev/test
|
|
34
|
-
steps grow to cover Postgres containers, database
|
|
35
|
-
creation, and migrations.
|
|
36
|
-
--auth Also scaffold Monk::Auth (passwordless token auth).
|
|
37
|
-
Implies --postgres -- Auth is Postgres-only, so this
|
|
38
|
-
always brings the persistence scaffold with it. Adds
|
|
39
|
-
config/auth.rb and a migration creating the
|
|
40
|
-
login_tokens/sessions tables it needs. config.ru
|
|
41
|
-
requires config/auth instead of config/persistence
|
|
42
|
-
(config/auth.rb itself requires persistence), and
|
|
43
|
-
.env/.env.test/.env.example get an AUTH_SECRET line.
|
|
44
|
-
Also implies --mail, below: config/auth.rb sends the
|
|
45
|
-
magic link through Monk::Mail, with an HTML part in
|
|
46
|
-
views/mail/magic_link.erb.
|
|
47
|
-
--mail Also scaffold Monk::Mail (text/HTML email over SMTP),
|
|
48
|
-
no Postgres needed: config/mail.rb, required by
|
|
49
|
-
config.ru, plus gem "net-smtp" in the Gemfile.
|
|
50
|
-
.env/.env.test/.env.example get MAIL_FROM, and
|
|
51
|
-
MAIL_URL where it matters: log:// in .env.test, an
|
|
52
|
-
smtp:// placeholder in .env.example. Unset in
|
|
53
|
-
development, MAIL_URL means log:// (messages printed,
|
|
54
|
-
not sent); anywhere else it must be set.
|
|
55
|
-
--redis Add the redis gem to the Gemfile, for
|
|
56
|
-
bin/websocket_server's cross-process fan-out. Fully
|
|
57
|
-
independent -- doesn't imply, and isn't implied by,
|
|
58
|
-
--postgres or --auth. Writes/extends .env and
|
|
59
|
-
.env.example with a REDIS_URL line (deliberately
|
|
60
|
-
never .env.test -- only a test that exercises
|
|
61
|
-
RedisFanout needs it) and uncomments gem "dotenv"
|
|
62
|
-
if it isn't already (same reasoning as --postgres
|
|
63
|
-
above), same whether or not --postgres is also set.
|
|
64
|
-
|
|
65
|
-
--live Also scaffold Monk::Live: a demo counter whose open tabs
|
|
66
|
-
update by themselves, pushed as HTML over a WebSocket.
|
|
67
|
-
Requires --redis or --postgres: bin/server and
|
|
68
|
-
bin/websocket_server are always separate processes, so
|
|
69
|
-
one of the two has to carry a publish between them --
|
|
70
|
-
with --redis, the usual Redis wiring; with --postgres
|
|
71
|
-
(and not --redis), Monk::WebSocket::PgFanout over
|
|
72
|
-
LISTEN/NOTIFY instead, reusing --postgres's own DB_*
|
|
73
|
-
settings, so there's no Redis to run. Passing both
|
|
74
|
-
picks --redis; passing neither is an error, since
|
|
75
|
-
there's no way to tell whether Postgres is available.
|
|
76
|
-
Adds
|
|
77
|
-
config/live.rb, views/live/_hits.erb and the browser
|
|
78
|
-
runtime under public/js/monk_live/, replaces config.ru,
|
|
79
|
-
views/index.erb and bin/websocket_server with live
|
|
80
|
-
versions, and adds the runtime's <meta>/<script> tags
|
|
81
|
-
to the layout.
|
|
82
|
-
--jobs Also scaffold Monk::Jobs (background jobs on Postgres).
|
|
83
|
-
Implies --postgres, where the queue lives. Adds
|
|
84
|
-
config/jobs.rb (required by config.ru, after
|
|
85
|
-
persistence/auth/mail), jobs/hello_job.rb (a demo
|
|
86
|
-
job, enqueued by a POST /jobs/hello route added to
|
|
87
|
-
config.ru), bin/jobs (the job process, run beside
|
|
88
|
-
bin/server), and the migration creating the queue's
|
|
89
|
-
tables. .env/.env.example get JOBS_WORKERS and
|
|
90
|
-
JOBS_QUEUES (not .env.test: tests run jobs with
|
|
91
|
-
Monk::Jobs.drain!).
|
|
92
|
-
|
|
93
|
-
How flags combine -- a flag is implied when there's only one right
|
|
94
|
-
answer, and required when there's a real choice:
|
|
95
|
-
|
|
96
|
-
Flag Implies Requires
|
|
97
|
-
--auth --postgres, --mail -
|
|
98
|
-
--jobs --postgres -
|
|
99
|
-
--live - --redis or --postgres (Live's
|
|
100
|
-
transport: a choice, so it's yours)
|
|
101
|
-
--postgres, --mail, --redis stand alone.
|
|
102
|
-
|
|
103
|
-
Some pairs also change what gets generated:
|
|
104
|
-
|
|
105
|
-
--auth + --jobs jobs/send_login_link.rb: login links sent from a job
|
|
106
|
-
--mail + --jobs config/jobs.rb loads Monk::Mail.deliver_later;
|
|
107
|
-
JOBS_QUEUES serves mailers first
|
|
108
|
-
--live + --redis config/live.rb fans out over Redis
|
|
109
|
-
--live + --postgres config/live.rb fans out over Postgres (without --redis)
|
|
110
|
-
|
|
111
|
-
`monk new` prints which flags it turned on, and why, after creating the
|
|
112
|
-
project.
|
|
113
|
-
|
|
114
|
-
Commands:
|
|
115
|
-
new APP_NAME Create a new project (see Options above)
|
|
116
|
-
help Show this help (also --help, -h)
|
|
117
|
-
|
|
118
|
-
After creating a project:
|
|
119
|
-
cd APP_NAME
|
|
120
|
-
bundle install
|
|
121
|
-
|
|
122
|
-
See the generated SETUP.md for a full dev-then-test walkthrough, tailored
|
|
123
|
-
to whichever flags were passed. With --postgres (or --auth, which implies
|
|
124
|
-
it), short version, once a reachable Postgres exists (.env/.env.test have
|
|
125
|
-
the connection details):
|
|
126
|
-
bin/setup_db Apply pending migrations
|
|
127
|
-
bin/migrate migrate | rollback [N] | status
|
|
128
|
-
bin/console IRB with persistence loaded
|
|
129
|
-
|
|
130
|
-
With --auth, .env/.env.test ship a placeholder AUTH_SECRET -- change it
|
|
131
|
-
before relying on it; config/auth.rb reads it via ENV.fetch and raises
|
|
132
|
-
if it's missing entirely.
|
|
133
|
-
|
|
134
|
-
With --redis, .env ships a placeholder REDIS_URL to turn
|
|
135
|
-
bin/websocket_server's cross-process fan-out on; unset (as in .env.test),
|
|
136
|
-
it runs in-process only, same as without --redis.
|
|
137
|
-
TEXT
|
|
138
|
-
|
|
139
|
-
def usage_error!
|
|
140
|
-
warn USAGE
|
|
141
|
-
exit 1
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
command, *rest = ARGV
|
|
145
|
-
|
|
146
|
-
case command
|
|
147
|
-
when nil, "help", "--help", "-h"
|
|
148
|
-
puts HELP
|
|
149
|
-
when "new"
|
|
150
|
-
app_name = rest.find { |arg| !arg.start_with?("-") }
|
|
151
|
-
usage_error! if app_name.nil?
|
|
152
|
-
|
|
153
|
-
# Flags as typed: Monk::Scaffold resolves what they imply, and reports it.
|
|
154
|
-
auth = rest.include?("--auth")
|
|
155
|
-
mail = rest.include?("--mail") || auth # for the next-steps lines below
|
|
156
|
-
redis = rest.include?("--redis")
|
|
157
|
-
live = rest.include?("--live")
|
|
158
|
-
jobs = rest.include?("--jobs")
|
|
159
|
-
scaffold = Monk::Scaffold.new(
|
|
160
|
-
app_name, postgres: rest.include?("--postgres"), auth: auth, mail: rest.include?("--mail"), redis: redis,
|
|
161
|
-
live: live, jobs: jobs,
|
|
162
|
-
)
|
|
163
|
-
scaffold.write!
|
|
164
|
-
|
|
165
|
-
puts "Created #{app_name}."
|
|
166
|
-
puts scaffold.summary
|
|
167
|
-
puts "Next steps:"
|
|
168
|
-
puts " cd #{app_name}"
|
|
169
|
-
puts " bundle install"
|
|
170
|
-
puts " see SETUP.md for the full dev-then-test walkthrough"
|
|
171
|
-
puts " change the placeholder AUTH_SECRET in .env/.env.test before relying on it" if auth
|
|
172
|
-
puts " set MAIL_URL and MAIL_FROM outside development (see config/mail.rb)" if mail
|
|
173
|
-
puts " run bin/jobs beside bin/server to process background jobs (see SETUP.md)" if jobs
|
|
174
|
-
if live
|
|
175
|
-
if redis
|
|
176
|
-
puts " Monk::Live needs Redis running, bin/server and bin/websocket_server (see SETUP.md)"
|
|
177
|
-
else
|
|
178
|
-
puts " Monk::Live needs bin/server and bin/websocket_server running, sharing Postgres (see SETUP.md)"
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
else
|
|
182
|
-
usage_error!
|
|
183
|
-
end
|
|
5
|
+
exit Monk::CLI.new.run(ARGV)
|
data/lib/monk/assets.rb
CHANGED
|
@@ -65,9 +65,28 @@ module Monk
|
|
|
65
65
|
# hazard (Monk.env resolves this the same way, through Settings'
|
|
66
66
|
# own frozen-at-Boot snapshot). `assets false` sets root to false
|
|
67
67
|
# and disables serving.
|
|
68
|
-
attr_reader :root, :manifest
|
|
68
|
+
attr_reader :root, :manifest, :mounts
|
|
69
69
|
attr_writer :root
|
|
70
70
|
|
|
71
|
+
# Serves a module's own static files from inside the gem, under a
|
|
72
|
+
# /_monk/ prefix, next to the app's public/: Monk::Live mounts its
|
|
73
|
+
# browser client at /_monk/live, so no app has to copy it
|
|
74
|
+
# (docs/adr/0017). Called when the module is required, before Boot;
|
|
75
|
+
# mounted files go in the same manifest, so they're served, stamped
|
|
76
|
+
# and cached exactly like the app's own.
|
|
77
|
+
def mount(prefix, dir)
|
|
78
|
+
prefix = prefix.chomp("/")
|
|
79
|
+
raise ArgumentError, "Monk::Assets.mount prefix must start with /_monk/, got #{prefix.inspect}" unless
|
|
80
|
+
prefix.start_with?("/_monk/")
|
|
81
|
+
|
|
82
|
+
@mounts = mounts.merge(prefix.freeze => File.expand_path(dir).freeze)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Test-only.
|
|
86
|
+
def unmount(prefix)
|
|
87
|
+
@mounts = mounts.except(prefix)
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
def enabled?
|
|
72
91
|
!!root
|
|
73
92
|
end
|
|
@@ -117,6 +136,7 @@ module Monk
|
|
|
117
136
|
# Called from Base#freeze! (Seam B), via Monk.freeze_hooks.
|
|
118
137
|
def freeze_registry!
|
|
119
138
|
@production = !Monk.env.development?
|
|
139
|
+
@mounts = Ractor.make_shareable(mounts)
|
|
120
140
|
@manifest = Ractor.make_shareable(build_manifest)
|
|
121
141
|
# #root is read on every request (#enabled?, and the disk lookup
|
|
122
142
|
# in development). Left unfrozen, reading it from a worker Ractor
|
|
@@ -125,7 +145,7 @@ module Monk
|
|
|
125
145
|
@root = root.freeze
|
|
126
146
|
end
|
|
127
147
|
|
|
128
|
-
# Test-only.
|
|
148
|
+
# Test-only. Mounts stay: a module mounts once, when it's required.
|
|
129
149
|
def reset!
|
|
130
150
|
@root = DEFAULT_ROOT
|
|
131
151
|
@manifest = {}
|
|
@@ -142,29 +162,38 @@ module Monk
|
|
|
142
162
|
private
|
|
143
163
|
|
|
144
164
|
def build_manifest
|
|
145
|
-
return {} unless enabled?
|
|
165
|
+
return {} unless enabled?
|
|
166
|
+
|
|
167
|
+
manifest = Dir.exist?(root) ? files_under(root, "") : {}
|
|
168
|
+
mounts.each { |prefix, dir| manifest.merge!(files_under(dir, prefix)) }
|
|
169
|
+
manifest
|
|
170
|
+
end
|
|
146
171
|
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
def files_under(dir, prefix)
|
|
173
|
+
Dir.glob("**/*", base: dir).each_with_object({}) do |relative, files|
|
|
174
|
+
path = File.join(dir, relative)
|
|
149
175
|
next unless File.file?(path)
|
|
150
176
|
|
|
151
|
-
|
|
177
|
+
files["#{prefix}/#{relative}"] = entry_for(relative, File.binread(path))
|
|
152
178
|
end
|
|
153
179
|
end
|
|
154
180
|
|
|
155
181
|
def lookup(path)
|
|
156
182
|
return manifest[path] if production?
|
|
157
183
|
|
|
158
|
-
|
|
184
|
+
prefix, dir = mounts.find { |mount_prefix, _| path.start_with?("#{mount_prefix}/") }
|
|
185
|
+
return disk_entry(dir, path.delete_prefix(prefix)) if prefix
|
|
186
|
+
|
|
187
|
+
disk_entry(root, path)
|
|
159
188
|
end
|
|
160
189
|
|
|
161
190
|
# Development only. PATH_INFO is not un-escaped here on purpose: an
|
|
162
191
|
# encoded "%2e%2e" stays a literal, nonexistent filename, and a
|
|
163
192
|
# decoded ".." is caught by the containment check below.
|
|
164
|
-
def disk_entry(path)
|
|
193
|
+
def disk_entry(dir, path)
|
|
165
194
|
return nil if path.empty? || path.include?("\0")
|
|
166
195
|
|
|
167
|
-
root_path = File.expand_path(
|
|
196
|
+
root_path = File.expand_path(dir)
|
|
168
197
|
full = File.expand_path(File.join(root_path, path.delete_prefix("/")))
|
|
169
198
|
return nil unless full.start_with?("#{root_path}#{File::SEPARATOR}")
|
|
170
199
|
return nil unless File.file?(full)
|
|
@@ -190,6 +219,7 @@ module Monk
|
|
|
190
219
|
|
|
191
220
|
@root = DEFAULT_ROOT
|
|
192
221
|
@manifest = {}
|
|
222
|
+
@mounts = {}
|
|
193
223
|
@production = false
|
|
194
224
|
|
|
195
225
|
Monk.freeze_hooks << self
|
data/lib/monk/auth/errors.rb
CHANGED
data/lib/monk/auth/helpers.rb
CHANGED
|
@@ -42,6 +42,17 @@ module Monk
|
|
|
42
42
|
add_response_cookie("csrf_token", "", http_only: false, max_age: 0)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Ends the current session: revokes it, whether its token came as a
|
|
46
|
+
# Bearer header or the session cookie, and clears the cookies -- with
|
|
47
|
+
# or without a session to end. On a cookie-authenticated route, call
|
|
48
|
+
# require_csrf! first.
|
|
49
|
+
def log_out!
|
|
50
|
+
token = bearer_token || session_cookie_token
|
|
51
|
+
Monk::Auth.revoke(token) if token
|
|
52
|
+
clear_session_cookie
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
private
|
|
46
57
|
|
|
47
58
|
def bearer_token
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Monk::Auth::SendLoginLink. Loaded at the end of whichever of monk/auth
|
|
2
|
+
# and monk/jobs is required second, once both are defined
|
|
3
|
+
# (docs/adr/0017), so it requires neither itself.
|
|
4
|
+
|
|
5
|
+
module Monk
|
|
6
|
+
module Auth
|
|
7
|
+
# Sends a magic link from bin/jobs instead of the login request. The
|
|
8
|
+
# token is created here, right before it's sent, so the raw token is
|
|
9
|
+
# never stored anywhere -- not even in the job queue, which pointing
|
|
10
|
+
# deliver: at Monk::Mail.deliver_later would do
|
|
11
|
+
# (docs/adr/0014-mail-from-jobs-and-login-links-created-in-the-job.md).
|
|
12
|
+
# Enqueue it from the app's login route, after its own per-email rate
|
|
13
|
+
# limit:
|
|
14
|
+
#
|
|
15
|
+
# post("/auth/request") do
|
|
16
|
+
# Monk::Auth::SendLoginLink.enqueue(params[:email])
|
|
17
|
+
# json(sent: true)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# The link is Monk::Auth.login_link (public_url + callback_path:), and
|
|
21
|
+
# it's sent through configure's deliver:, synchronously, in the job.
|
|
22
|
+
# Subclass it to change the queue or the retries.
|
|
23
|
+
class SendLoginLink < Monk::Job
|
|
24
|
+
queue "mailers"
|
|
25
|
+
# About 50 seconds of retries: a login email minutes late, after the
|
|
26
|
+
# user has likely asked for another, is worse than none.
|
|
27
|
+
max_attempts 3
|
|
28
|
+
never_retry Monk::InvalidRedirectError
|
|
29
|
+
|
|
30
|
+
def self.perform(email, redirect_to = nil)
|
|
31
|
+
token = Monk::Auth.request_login(email, redirect_to: redirect_to)
|
|
32
|
+
Monk::Auth.deliver_link(email: email, link: Monk::Auth.login_link(token), token: token)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/monk/auth.rb
CHANGED
|
@@ -34,13 +34,18 @@ module Monk
|
|
|
34
34
|
freeze_rqrcode!
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# callback_path: is where the app's own callback route redeems a
|
|
38
|
+
# token (GET "#{callback_path}/:token"). Monk doesn't define that
|
|
39
|
+
# route, but builds links to it: login_link below, and so
|
|
40
|
+
# Monk::Auth::SendLoginLink.
|
|
37
41
|
def configure(
|
|
38
42
|
db_name: nil, secret: nil, login_ttl: nil, session_ttl: nil, redirect_allowlist: [], secure: true,
|
|
39
|
-
deliver: nil
|
|
43
|
+
deliver: nil, callback_path: "/auth/callback"
|
|
40
44
|
)
|
|
41
45
|
config = {
|
|
42
46
|
db_name: db_name, secret: secret, login_ttl: login_ttl, session_ttl: session_ttl,
|
|
43
47
|
redirect_allowlist: redirect_allowlist, secure: secure, deliver: deliver,
|
|
48
|
+
callback_path: checked_callback_path(callback_path),
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
REQUIRED_CONFIG_KEYS.each do |key|
|
|
@@ -81,6 +86,17 @@ module Monk
|
|
|
81
86
|
raw
|
|
82
87
|
end
|
|
83
88
|
|
|
89
|
+
# The magic link for a raw token: the app's public_url
|
|
90
|
+
# (config/settings.rb, a trusted origin -- never request headers like
|
|
91
|
+
# X-Forwarded-Host, which any direct client can spoof), then
|
|
92
|
+
# callback_path:, then the token.
|
|
93
|
+
#
|
|
94
|
+
# Monk::Auth.login_link(token) # => "https://app.example/auth/callback/<token>"
|
|
95
|
+
def login_link(token)
|
|
96
|
+
ensure_configured!
|
|
97
|
+
"#{Monk::Settings[:public_url]}#{config[:callback_path]}/#{token}"
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
# The single place an app's login-request handler calls to get a
|
|
85
101
|
# magic link to its owner: uses the `deliver:` callable passed to
|
|
86
102
|
# `configure` (e.g. wrapping a mailer or a provider's HTTP API) when
|
|
@@ -94,10 +110,8 @@ module Monk
|
|
|
94
110
|
# that Monk::Mail exists (docs/adr/0012-minimal-built-in-mailer.md):
|
|
95
111
|
# it's usually a one-line Monk::Mail.deliver, but can be any channel.
|
|
96
112
|
#
|
|
97
|
-
# `link` is
|
|
98
|
-
#
|
|
99
|
-
# Monk::Settings[:public_url], never from request headers like
|
|
100
|
-
# X-Forwarded-Proto/Host, which any direct client can spoof.
|
|
113
|
+
# `link` is usually login_link(token), above. An app may build its
|
|
114
|
+
# own instead, from a trusted origin, never from request headers.
|
|
101
115
|
# `token` is passed through for a deliver: that wants it (an SMS
|
|
102
116
|
# body instead of a link, say); most won't need it.
|
|
103
117
|
def deliver_link(email:, link:, token: nil)
|
|
@@ -254,6 +268,17 @@ module Monk
|
|
|
254
268
|
"Monk::Auth is not configured -- call Monk::Auth.configure first" unless @config
|
|
255
269
|
end
|
|
256
270
|
|
|
271
|
+
# An absolute path, kept without a trailing slash so login_link
|
|
272
|
+
# adds exactly one before the token.
|
|
273
|
+
def checked_callback_path(path)
|
|
274
|
+
unless path.is_a?(String) && path.start_with?("/")
|
|
275
|
+
raise Monk::InvalidAuthConfigError,
|
|
276
|
+
"Monk::Auth.configure(callback_path:) must be an absolute path like \"/auth/callback\", got #{path.inspect}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
path.chomp("/").freeze
|
|
280
|
+
end
|
|
281
|
+
|
|
257
282
|
def hash_token(raw)
|
|
258
283
|
Digest::SHA256.hexdigest(raw)
|
|
259
284
|
end
|
|
@@ -299,3 +324,7 @@ module Monk
|
|
|
299
324
|
Monk.freeze_hooks << self
|
|
300
325
|
end
|
|
301
326
|
end
|
|
327
|
+
|
|
328
|
+
# Monk::Auth::SendLoginLink, when jobs are loaded too -- either order works
|
|
329
|
+
# (lib/monk/jobs.rb ends the same way, docs/adr/0017).
|
|
330
|
+
require_relative "auth/send_login_link" if Monk.const_defined?(:Jobs)
|