strapi-plugin-magic-link-v5 5.5.9 → 6.0.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 (58) hide show
  1. package/CHANGELOG.md +733 -0
  2. package/README.md +662 -928
  3. package/dist/admin/LicenseGuard-BgLA9go3.mjs +495 -0
  4. package/dist/admin/LicenseGuard-ewCjpc17.js +513 -0
  5. package/dist/admin/de-B1FNErPH.mjs +673 -0
  6. package/dist/admin/de-e75OECdq.js +673 -0
  7. package/dist/admin/en-CABKGbxH.js +673 -0
  8. package/dist/admin/en-HqDOFBCc.mjs +673 -0
  9. package/dist/admin/es-DpWl0Fax.js +673 -0
  10. package/dist/admin/es-jb94jqPR.mjs +673 -0
  11. package/dist/admin/fr-BQhLPvYw.js +673 -0
  12. package/dist/admin/fr-BseH1wrr.mjs +673 -0
  13. package/dist/admin/index-6ot9j1cN.js +5268 -0
  14. package/dist/admin/index-B390rccl.js +622 -0
  15. package/dist/admin/index-BPGKtruf.mjs +5266 -0
  16. package/dist/admin/index-BaTIQtnA.js +3869 -0
  17. package/dist/admin/index-CmG-93yx.js +141 -0
  18. package/dist/admin/index-DXe_ge4v.mjs +620 -0
  19. package/dist/admin/index-Dv5PVT1E.mjs +3867 -0
  20. package/dist/admin/index-k240ls6p.mjs +142 -0
  21. package/dist/admin/index.js +3 -3
  22. package/dist/admin/index.mjs +1 -2
  23. package/dist/admin/pt-CPkaveti.js +673 -0
  24. package/dist/admin/pt-DhlSCJn2.mjs +673 -0
  25. package/dist/admin/useUnsavedChangesGuard-BVZ4Mg4E.js +225 -0
  26. package/dist/admin/useUnsavedChangesGuard-QnkiZTLP.mjs +224 -0
  27. package/dist/server/i18n/de.json +32 -1
  28. package/dist/server/i18n/en.json +32 -1
  29. package/dist/server/i18n/es.json +32 -1
  30. package/dist/server/i18n/fr.json +32 -1
  31. package/dist/server/i18n/pt.json +32 -1
  32. package/dist/server/index.js +7092 -5971
  33. package/dist/server/index.mjs +7115 -5972
  34. package/package.json +42 -41
  35. package/strapi-admin.js +3 -1
  36. package/strapi-server.js +3 -2
  37. package/dist/_chunks/LicenseGuard-CiyjFnrw.js +0 -54
  38. package/dist/_chunks/LicenseGuard-DJ5fq2UZ.mjs +0 -55
  39. package/dist/_chunks/de-BSOk5suQ.js +0 -454
  40. package/dist/_chunks/de-CwhYs3O3.mjs +0 -454
  41. package/dist/_chunks/en-DnvSpXvu.js +0 -454
  42. package/dist/_chunks/en-gXXgGDV1.mjs +0 -454
  43. package/dist/_chunks/es-B-tnwByt.js +0 -407
  44. package/dist/_chunks/es-DG5gW3RZ.mjs +0 -407
  45. package/dist/_chunks/fr-BKo8t7Fx.js +0 -407
  46. package/dist/_chunks/fr-BQn1OHOR.mjs +0 -407
  47. package/dist/_chunks/index-01GHNxSr.js +0 -307
  48. package/dist/_chunks/index-5jsDgD-q.mjs +0 -330
  49. package/dist/_chunks/index-BWuj-5WE.js +0 -5068
  50. package/dist/_chunks/index-Bj3H_Z4B.js +0 -142
  51. package/dist/_chunks/index-CC4Eu9Dm.js +0 -332
  52. package/dist/_chunks/index-Csyi13vd.mjs +0 -2891
  53. package/dist/_chunks/index-DJqXiiP1.js +0 -2893
  54. package/dist/_chunks/index-DQUYFsLT.mjs +0 -143
  55. package/dist/_chunks/index-Dufv-daf.mjs +0 -305
  56. package/dist/_chunks/index-szeYJKpN.mjs +0 -5066
  57. package/dist/_chunks/pt-CjEkXBAz.js +0 -407
  58. package/dist/_chunks/pt-trziAOzT.mjs +0 -407
package/CHANGELOG.md CHANGED
@@ -1,3 +1,736 @@
1
+ # [6.0.0](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.6.0...v6.0.0) (2026-11-07)
2
+
3
+
4
+ * build!: drop unused dependencies, require Node 22 and harden CI and release ([05eabb9](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/05eabb96baf98a8cca292f561a6e687d9417aeb8))
5
+ * build(deps)!: declare the users-permissions peer dependency ([fde4798](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/fde4798c273a9aa8e4d3ddd1144af8846c2eaaf6)), closes [#peerdependencies](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/peerdependencies)
6
+ * feat(admin-api)!: enforce read/manage RBAC, guard impersonation and harden sessions, IP bans, limits and license ([3036538](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/30365384350723d2ef38035a34f0cc81a03f8b5f))
7
+ * feat(admin)!: split admin RBAC into read, manage and settings actions ([8a0a5f3](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/8a0a5f3249dd8b0dd56585394dcd64d285e18a6b))
8
+ * feat(auth)!: harden the login core and make Magic Link e-mail only ([a1f4b89](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/a1f4b89b348fd046796658db22e9b52771478ead))
9
+ * feat(mfa)!: namespace TOTP by principal, add enrolment and backup codes, move to otplib ([466319e](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/466319e332c0fdc597ef393ea16a9e434f57fa87))
10
+ * fix(admin-api)!: require a confirmation for reset-data and keep MFA settings ([258fc76](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/258fc76c746e6a2c5e43d37108fcd8127a4e571b))
11
+ * fix(admin)!: rebuild the Settings view with lock-out safe MFA modes and an unsaved-changes guard ([2942a75](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2942a75d3b0749c435e5dad4d7e449ddc2449ea6))
12
+ * fix(security)!: close revocation path bypass and admin limiter/settings gaps ([f63d203](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/f63d203eeb4315f304516280b22615bcc589cfda))
13
+ * fix(security)!: close the open adversarial review findings ([e1a19b5](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/e1a19b5ae71c9bf1789c1e338fae2b5278b34766))
14
+ * fix(security)!: guard required MFA, hash the send-link limiter key, tighten settings.security ([720240f](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/720240fb30927f5c1df46fd75d6ec081936f9f3a))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **admin:** complete translations, formal German and safe template placeholders ([e3e13fd](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/e3e13fde1daa9c8d24c485994e0661ba4ed34c4b))
20
+ * **admin:** make the Tokens list keyboard operable, readable and responsive ([8315dd2](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/8315dd2baf39cf698ec04423c0eba868b47dc45a)), closes [#0284C7](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/0284C7) [#0369A1](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/0369A1) [#16A34A](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/16A34A) [15803D/#166534](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/166534)
21
+ * **admin:** mask license key, gate it by settings rights and localise the License page ([a65011f](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/a65011f621fcc653c771b57a0380d14cd23f8f59))
22
+ * **admin:** pass browser acceptance of the OTP, JWT session and IP ban tabs ([40b1205](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/40b1205cc280e9c7d88c6ebdba419005439ae35b)), closes [#0369A1](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/0369A1) [#9333EA](https://github.com/Schero94/strapi-plugin-magic-link-v5/issues/9333EA)
23
+ * **admin:** pass browser acceptance of the Settings and License pages ([782bde7](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/782bde71f12c80cb29fd92a73144748c9e246eea))
24
+ * **admin:** rebuild OTP, JWT session and IP ban tabs on the shared foundation ([e9c85ed](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/e9c85ed55367df7941e32e12772cd2e968a13df8))
25
+ * **admin:** rebuild the Tokens view on the shared foundation and fix token actions ([afcd4d6](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/afcd4d6f793a342c210a1125945b8dc8ac12223e))
26
+ * **admin:** show IPv4-mapped bans as IPv4 and drop a stale validator ([73add59](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/73add59c2eb6cd83dbb8f9d6fd2e98b9c67f888a))
27
+ * **admin:** stop the reload permission race and unstick the Tokens table keyboard ([b76ff5b](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/b76ff5b8f024f15caef6a7cdd3105e8dd23e6d42))
28
+ * **auth:** give every legacy JWT a unique jti so session revocation is per session ([2100678](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2100678098d8f38faf4325f3854bb2069e90a22d))
29
+ * **auth:** run the JWT revocation check on Content API paths only ([2189a36](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/2189a3642e944d44d1a17a3ef55b450a1d45d10d))
30
+ * **errors:** keep the IP-ban and tokens.issue 403 details; final docs review ([b336280](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/b3362801714792f2eed9fc776c9c224283d827be))
31
+ * **i18n:** merge view translations, drop unused keys and enforce id coverage ([9b949a4](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/9b949a44bd36d27e8a8ac3e40a5fc65848d0bed6))
32
+ * **mail:** send Magic Mail login links as untracked transactional mail ([1b42439](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/1b424395f1d72c8fead3ce98a835f458823002b3))
33
+ * **mfa:** keep second-factor brute-force limits on when rate limiting is off ([5ba821e](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/5ba821ec5130f5558f77cbc1b63bd6b94ba464c7))
34
+ * **otp:** accept the admin status filter on GET /otp/codes and return stats ([132a149](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/132a14949d204995417780ad85d3c1749870fe40))
35
+ * **release:** pass the npm token as fallback to trusted publishing ([72ca270](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/72ca270201ae8ddd5ac83dc7c146515066eea217))
36
+ * **security:** strict GCM tags, refresh-family session records, drop dead OTP mailer ([d12d215](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/d12d215e32c84564e27e48d2b970a8a80209efaa))
37
+
38
+
39
+ ### Documentation
40
+
41
+ * rewrite README and project site for the e-mail-only 6.0.0 release ([4744ef6](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/4744ef6035c53cd4ab3305823918cba85195844f))
42
+
43
+
44
+ ### Features
45
+
46
+ * **admin:** add shared admin foundation for theme, i18n, dialogs and RBAC ([4e6527b](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/4e6527b3d1de774b30c507ca3534aa7e9c218350))
47
+
48
+
49
+ ### BREAKING CHANGES
50
+
51
+ * POST /magic-link/reset-data needs plugin::magic-link.settings.security and no longer returns securitySettingsKept; changing from_name, from_email or response_email needs settings.security; switching mfa_mode to required with no enrolled user needs acknowledgeNoEnrolledUsers: true; max_login_attempts is no longer accepted or stored.
52
+
53
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54
+ * changing OTP, TOTP-parameter and rate-limit settings now
55
+ needs plugin::magic-link.settings.security, and reset-data without that
56
+ permission keeps all security settings. Grant Settings > Security to roles
57
+ that should keep changing them.
58
+
59
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60
+ * changing security settings (link URLs and paths,
61
+ templates, template providers, account creation, link/session lifetimes,
62
+ context lists, MFA/OTP switches) now requires
63
+ plugin::magic-link.settings.security in addition to settings.update; the
64
+ action is never granted automatically. Confirming an unconfirmed account
65
+ via Magic Link clears its password and ends its sessions. IPv6 IP bans now
66
+ cover the whole /64 network.
67
+
68
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69
+ * hosts must have @strapi/plugin-users-permissions ^5.24.0
70
+ installed (it ships with every default Strapi 5 project).
71
+
72
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73
+ * POST /magic-link/reset-data requires the body
74
+ { "confirm": "RESET" }; MFA settings survive a reset unless
75
+ "resetSecurity": true is sent.
76
+
77
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78
+ * documented 6.0.0 contract. Magic Link is e-mail only and
79
+ needs Node.js 22+ and Strapi 5.24+; see the upgrade guide in README.md and
80
+ CHANGELOG.md.
81
+
82
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83
+ * Node.js 22 or newer is required, and the plugin needs
84
+ Strapi 5.24.0 or newer (users-permissions session manager).
85
+
86
+ Docs: npm trusted publishers (docs.npmjs.com/trusted-publishers),
87
+ GitHub Actions security hardening (pin to full SHA, least-privilege
88
+ GITHUB_TOKEN), Strapi 5.61.0 package.json engines, npm registry
89
+ metadata for qrcode and @paulmillr/qr; checked 2026-11-07.
90
+
91
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92
+ * admin routes require plugin::magic-link.read / manage /
93
+ settings.read / settings.update (5.x `access` is migrated to read + manage
94
+ when the permission rows still exist). POST /magic-link/tokens returns the
95
+ plaintext token only for send_email:false and only with the new
96
+ plugin::magic-link.tokens.issue action, sends `ttl` top-level, fails with
97
+ 503 when the mail fails; several unused admin routes were removed; GET
98
+ /tokens no longer returns hashes or securityScore; revoke-jwt needs
99
+ sessionId xor token; IPv4-mapped addresses are matched as IPv4; IP-ban
100
+ denials are formatted 403s with details.code; unknown settings keys are
101
+ dropped; license status masks the key for settings readers and no host
102
+ telemetry is sent. See CHANGELOG [Unreleased] for migration steps.
103
+
104
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105
+ * MAGIC_LINK_ENCRYPTION_KEY and MAGIC_LINK_OTP_PEPPER are
106
+ required (>= 32 characters) on every host; rotate short keys with
107
+ MAGIC_LINK_ENCRYPTION_KEY_PREVIOUS. TOTP enrolments are namespaced by
108
+ principal and 5.x rows without provable ownership are disabled. Required
109
+ MFA answers /login with requiresTOTPSetup instead of 403. /otp/verify
110
+ returns the common login body, the unbound OTP flow is refused while MFA
111
+ is active, otp-code lost the sms type and phoneNumber, admin TOTP routes
112
+ need a current code for re-setup, disable and backup codes. Grant the
113
+ `totp` actions to the Authenticated role for self-service. See CHANGELOG.
114
+
115
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116
+ * the Settings page no longer offers the login_path,
117
+ callback_url, max_login_attempts and Email Designer compatibility
118
+ controls (the server never used them), and saves only changed fields.
119
+ Choosing "Magic link + authenticator app" now selects the optional
120
+ policy; requiring TOTP for every user needs an explicit confirmation.
121
+
122
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123
+ * WhatsApp and SMS delivery removed; send-link requires email and rejects phoneNumber, phone, via and non-email delivery with 400; whatsapp_* and sms_* settings are deleted and otp_type only accepts 'email'.
124
+ * reserved context keys (requiresOTP, otpVerified, requiresTOTP, totpVerified, mfaVerified, totpLogin, userId, userDocumentId, user, expires_at, expiresAt, ttl and JWT claim names) are rejected; public context follows context_whitelist/context_blacklist.
125
+ * e-mail templates use {{VARIABLE}} placeholders; legacy <%= VAR %> is converted, any other <% %> code is printed as text.
126
+ * login links follow frontend_url+frontend_login_path > confirmationUrl > API URL; link URL settings must be https (http only for localhost); token_length must be 22..128.
127
+ * login responses return a content-API sanitized user, drop expiry_formatted and the TOTP challenge userId, return jwtIssued:false when use_jwt_token is false, and use the Strapi error envelope.
128
+ * JWTs honour jwt_token_expires_in; in users-permissions refresh mode logins return refreshToken (or the refresh cookie) from the u-p session manager.
129
+ * users with an enabled authenticator always get the TOTP challenge; required MFA without an authenticator returns 403.
130
+ * auto-registration also requires users-permissions allow_register; new users start unconfirmed; TOTP-only login no longer confirms users.
131
+ * /api/magic-link/passwordless/* routes and the compatibility-check policy are removed; /<prefix>/passwordless/* is handled by the rewrite middleware.
132
+
133
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134
+ * the plugin::magic-link.access action is replaced by
135
+ plugin::magic-link.read and plugin::magic-link.manage, and the License page
136
+ requires plugin::magic-link.settings.read. Re-grant Read (and Manage) to
137
+ every role that had "Access the Magic Link plugin"; see CHANGELOG.
138
+
139
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140
+
141
+ ## [Unreleased]
142
+
143
+ Next release: **6.0.0** (major). Magic Link becomes e-mail only and is
144
+ hardened throughout. Every breaking change below has migration steps; the
145
+ short version is the upgrade guide.
146
+
147
+ ### Upgrade guide (5.x to 6.0.0)
148
+
149
+ 1. Run Node.js 22 or newer and Strapi 5.24.0 or newer (5.33.3+ when refresh
150
+ mode should read the refresh token from the httpOnly cookie).
151
+ 2. Set `MAGIC_LINK_ENCRYPTION_KEY` and `MAGIC_LINK_OTP_PEPPER` (32+ characters
152
+ each) on every host. If the old encryption key was shorter, or you used the
153
+ development fallback, also set `MAGIC_LINK_ENCRYPTION_KEY_PREVIOUS` for the
154
+ first start (see "Secrets are mandatory" below).
155
+ 3. Remove WhatsApp/SMS fields from `send-link` calls and delete a leftover
156
+ `.whatsapp-auth` directory.
157
+ 4. Review admin roles: `access` became `read`, `manage`, `settings.read`,
158
+ `settings.update`, `tokens.issue` and `settings.security` (roles with
159
+ `access` get Read and Manage automatically; the others are never granted
160
+ automatically).
161
+ 5. Frontend: exchange tokens with `POST /api/magic-link/login { loginToken }`,
162
+ handle `requiresTOTPSetup` when MFA is required, use `expires_at` instead of
163
+ `expiry_formatted`, and read errors from `error.key`.
164
+ 6. Replace JavaScript in e-mail templates with `{{PLACEHOLDERS}}`.
165
+ 7. Use `https` for `frontend_url`, `confirmationUrl` and `callback_url`
166
+ (http only for localhost).
167
+ 8. Grant the `totp` actions to the Authenticated role for authenticator
168
+ self-service.
169
+ 9. Admin token API clients: send `ttl` (hours) as a top-level field.
170
+
171
+ ### BREAKING CHANGES (requirements)
172
+
173
+ * **Node.js 22 or newer** (`engines.node` is `>=22.0.0 <=26.x.x`, the range
174
+ Strapi 5.61 declares). Node 18 and 20 are no longer supported.
175
+ * **Strapi 5.24.0 or newer** (`@strapi/strapi`, `@strapi/utils` and, new,
176
+ `@strapi/plugin-users-permissions` peers `^5.24.0`). 5.24.0 is the first release with the users-permissions session
177
+ manager that refresh-mode logins use.
178
+ * **E-mail only.** WhatsApp and SMS delivery are removed from the API, the
179
+ settings, the admin panel, the translations and the documentation (details
180
+ under "login core").
181
+
182
+ ### BREAKING CHANGES (admin RBAC)
183
+
184
+ * **Admin RBAC split into read, manage and settings actions.** The single
185
+ `plugin::magic-link.access` action is replaced by:
186
+ * `plugin::magic-link.read`: view tokens, OTP codes, JWT sessions and IP bans (menu link and Tokens page)
187
+ * `plugin::magic-link.manage`: create, block, extend and delete tokens, revoke sessions, ban and unban IPs, delete OTP codes, cleanups
188
+ * `plugin::magic-link.settings.read`: view the Settings and License pages (License now needs this instead of `access`)
189
+ * `plugin::magic-link.settings.update`: save settings, reset rate limits, change the license
190
+ * `plugin::magic-link.tokens.issue` (new, *Tokens > Issue login links without
191
+ e-mail*): needed for `POST /magic-link/tokens` with `send_email: false`,
192
+ which hands a working login link for the user to the admin (impersonation)
193
+
194
+ The server enforces these actions on every admin route.
195
+
196
+ Migration: on startup, roles that still hold the 5.x `access` permission
197
+ get **Read** and **Manage** automatically and the old permission is removed;
198
+ `tokens.issue` is never granted automatically. If your roles show no Magic
199
+ Link permission after the upgrade, open every role that had "Access the
200
+ Magic Link plugin" in *Settings > Administration Panel > Roles* and grant
201
+ **Read** (and **Manage** for roles that changed tokens, sessions, IP bans or
202
+ OTP codes). Grant Settings **Read** to roles that used the License page and
203
+ **Issue login links** only to roles that may log in as end users. Super
204
+ Admin gets all actions automatically.
205
+ * **New `plugin::magic-link.settings.security` action** (*Settings > Security*).
206
+ Changing a setting that can redirect login links (and the token in them),
207
+ put the raw token into a template, or change who may log in and with which
208
+ factors now needs it in addition to `settings.update`: `confirmationUrl`,
209
+ `callback_url`, `frontend_url`, `frontend_login_path`, `login_path`,
210
+ `passwordlessCompatibility`, `message_html`, `message_text`, the Email
211
+ Designer / Magic Mail template fields, `enabled`, `createUserIfNotExists`,
212
+ `user_creation_strategy`, `allow_magic_links_on_public_registration`,
213
+ `verify_email`, `stays_valid`, `expire_period`, `token_length`,
214
+ `use_jwt_token`, `jwt_token_expires_in`, `context_whitelist`,
215
+ `context_blacklist`, `mfa_mode`, `mfa_require_totp`,
216
+ `totp_as_primary_auth`, `otp_enabled`, `otp_strict_binding`, and the code
217
+ and brute-force rules `otp_length`, `otp_expiry`, `otp_max_attempts`,
218
+ `otp_resend_cooldown`, `totp_algorithm`, `totp_digits`, `totp_period`,
219
+ `rate_limit_enabled`, `rate_limit_max_attempts`,
220
+ `rate_limit_window_minutes`, `rate_limit_login_*`, and the sender identity
221
+ `from_name`, `from_email` and `response_email`. Sending them unchanged is allowed. A refused save
222
+ answers 403 (`error.details.key: settings.security.forbidden`, `fields`)
223
+ and writes nothing. `reset-data` needs it as well (see "Follow-up fixes"
224
+ below).
225
+ Migration: the action is never granted automatically (Super Admin has
226
+ it). Grant **Settings > Security** to the roles that should keep changing
227
+ link targets, templates, account, MFA, code or rate-limit rules; roles
228
+ with only `settings.update` keep subject, welcome-mail, login-info and TOTP
229
+ issuer changes.
230
+ * **Optional `allowedLinkOrigins` plugin config.** When set
231
+ (`config/plugins.js`, e.g. `['https://app.example.com']`), a changed
232
+ `confirmationUrl`, `frontend_url` or `callback_url` outside these origins
233
+ is rejected with 400 (`settings.linkOrigin.notAllowed`), whatever the
234
+ admin's permissions.
235
+
236
+ ### BREAKING CHANGES (admin API)
237
+
238
+ * **Admin-created tokens.** `POST /magic-link/tokens` is validated
239
+ (`email`, `ttl` in hours 1-720, `send_email`, `context` as object or JSON
240
+ string; unknown fields and reserved context keys return 400). It uses the
241
+ same renderer, link builder and providers (Magic Mail, Email Designer,
242
+ e-mail plugin) as the public send-link. With `send_email: true` the
243
+ response no longer contains the plaintext token; with `send_email: false`
244
+ it contains `token` and `loginUrl` once and requires
245
+ `plugin::magic-link.tokens.issue` (403 otherwise). A failed e-mail
246
+ deactivates the new token and returns 503 instead of a silent success.
247
+ Missing users are created only when `createUserIfNotExists` is on, and
248
+ start unconfirmed. Admin creation is limited to 60 per admin and 15
249
+ minutes and no longer uses the public per-IP/per-e-mail buckets. Each issue
250
+ is logged with the admin id and token id (no e-mail, no token).
251
+ Migration: send `ttl` as a top-level field (not in `context`); grant
252
+ `tokens.issue` to roles that need copyable links.
253
+ * **Token list and actions.** `GET /magic-link/tokens` no longer returns the
254
+ hash, salt, lookup prefix or `challenge`, and no `meta.securityScore`; it
255
+ returns the newest 1000 tokens, or a page with `?page=&pageSize=`
256
+ (`meta.pagination`). `extend` accepts `days` 1-365 (400 otherwise) and
257
+ refuses used single-use links. `activate` refuses used single-use links,
258
+ links that completed a second-factor challenge, expired links and links
259
+ last used from a banned IP.
260
+ * **Removed admin routes** (unused by the admin UI): `GET /security-score`,
261
+ `GET /validate-email`, `GET /user-by-email`, `POST /tokens/:id/resend`,
262
+ `POST /license/auto-create`, `POST /license/create`, `POST /license/ping`.
263
+ * **JWT sessions.** `POST /revoke-jwt` needs exactly one of `sessionId` or
264
+ `token`; a client-sent `userId` is ignored and the acting admin is recorded
265
+ (`revokedBy`). Refresh-mode sessions cannot be restored with
266
+ `/unrevoke-jwt` (the users-permissions session is deleted on revocation).
267
+ Plaintext JWTs stored by older releases in the session list are replaced
268
+ by their hash on startup; records are kept 7 days after expiry.
269
+ * **IP addresses.** IPv4-mapped IPv6 addresses (`::ffff:192.0.2.1`) are
270
+ stored and matched as IPv4, so an IPv4 ban also blocks dual-stack
271
+ requests. Rate limits key IPv6 clients by their /64 network. Banned or
272
+ unverifiable requests now get a formatted 403 `PolicyError` with
273
+ `error.details.code` `IP_ADDRESS_BLOCKED` or `IP_BAN_CHECK_UNAVAILABLE` (was a generic
274
+ "Policy Failed").
275
+ * **IPv6 bans cover the /64.** Bans now match with the same granularity as
276
+ the rate limits: IPv4 exactly, IPv6 per /64 network. Banning one IPv6
277
+ address blocks every address of its /64; unbanning any address of that
278
+ /64 removes all stored bans of it. Migration: review IPv6 entries in
279
+ *IP bans*; an entry now also blocks its neighbours in the same /64.
280
+ * **Settings API.** Only known settings keys are stored and returned; unknown
281
+ and legacy keys (`create_new_user`, `remember_me`, `token_lifetime`, ...)
282
+ are dropped on the next save. `context_whitelist`/`context_blacklist`
283
+ strings are stored as arrays. `reset-data` writes the same defaults as a
284
+ fresh install (API login URL, no `noreply@example.com`).
285
+ * **`POST /magic-link/reset-data` needs a confirmation.** The body must be
286
+ `{ "confirm": "RESET" }` (400 otherwise, nothing is deleted). The MFA
287
+ settings (`mfa_mode`, `mfa_require_totp`, `totp_as_primary_auth`,
288
+ `otp_enabled`) are kept unless `"resetSecurity": true` is sent, so a reset
289
+ never switches MFA off silently.
290
+ Migration: scripts that call the route send the confirmation body.
291
+ * **License privacy.** The plugin no longer sends hostname, IP address, OS
292
+ details, a device id or admin name/e-mail to the license server; only the
293
+ license key is sent (on save and for the License page, cached 10 minutes).
294
+ Set `license: { remoteLookup: false }` in the plugin config or
295
+ `MAGIC_LINK_LICENSE_LOOKUP=false` to disable all license-server calls.
296
+ `GET /license/status` returns `licenseKeyMasked` and `lookup`
297
+ (`ok`, `not-found`, `unavailable`, `disabled`, `none`); the full key and
298
+ holder name/e-mail are only returned to admins with `settings.update`.
299
+ `POST /license/store-key` validates the key format.
300
+
301
+ ### Security and fixes (admin API)
302
+
303
+ * Session-list and IP-ban updates are serialised, so parallel revocations,
304
+ logins and bans no longer overwrite each other in one process.
305
+ * Saving settings keeps rate-limit counters unless a limit changed;
306
+ `/rate-limit/stats` reports real rejection counts. Optional shared limits
307
+ for clusters: `rateLimit: { redis: { storeClient } }` in the plugin config
308
+ (rate-limiter-flexible Redis limiter with an in-memory fallback).
309
+ * A startup warning appears when `server.proxy.koa` is on without
310
+ `server.proxy.maxIpsCount` (clients could spoof their IP).
311
+ * Bootstrap timers are cleared in `destroy()`, and failing background jobs
312
+ are logged instead of causing unhandled rejections.
313
+ * Admin logs no longer contain settings objects, e-mail addresses or tokens.
314
+ * The always-on code-check limiter (`verify`, 10 checks per account and
315
+ 15 minutes) and the admin limiter are no longer cleared by
316
+ `POST /rate-limit/reset` or by changing the public rate-limit values.
317
+ Before, a role with `settings.update` could reset them in a loop and give
318
+ unlimited guesses at TOTP, backup and e-mail codes. The reset now clears
319
+ only the public limiters.
320
+ * Rate-limit admin endpoints no longer log or return limiter error details
321
+ (a Redis error can contain the connection URL).
322
+ * A JWT session ended by an account claim can no longer be restored with
323
+ `unrevoke-jwt` (400).
324
+ * `GET /otp/codes` accepts the `status` filter (`active`, `expired`, `used`)
325
+ that the admin OTP tab sends. Before, the strict query schema rejected it
326
+ with 400, so every status filter failed. The response also carries
327
+ `stats` (`total`, `active`, `expired`, `used` over all codes), so the stat
328
+ cards stay filled while a search or filter is active.
329
+ * `GET /jwt-sessions` adds `restoreBlock` per session (`expired`,
330
+ `refreshMode`, `accountClaim` or `null`), with the same checks as
331
+ `unrevoke-jwt`. Refresh-mode sessions cannot be restored because the
332
+ users-permissions session manager deletes the session on revocation.
333
+ * `PUT /settings` and `POST /reset-data` reject security changes without
334
+ `settings.security` with a `PolicyError` (403) that keeps `message` and
335
+ `details` (`key: 'settings.security.forbidden'`, `fields`). Strapi replaced
336
+ the previous `ForbiddenError` with a bare "Forbidden" and dropped the
337
+ details, so the admin could not say what was refused.
338
+ * The IP-ban policy (`IP_ADDRESS_BLOCKED`) and `POST /magic-link/tokens`
339
+ without `tokens.issue` (`tokens.issue.forbidden`) now throw a
340
+ `PolicyError` (403) as well. Strapi 5.61's authorize middleware wraps the
341
+ policies and the controller, so the previous `ForbiddenError` reached
342
+ clients as a bare "Forbidden" with empty `details` (seen on a 5.61 host:
343
+ a banned IP got `{"name":"ForbiddenError","message":"Forbidden","details":{}}`).
344
+ Clients that read `error.details.code` or `error.details.key` now get it;
345
+ `error.name` is `PolicyError`.
346
+
347
+ ### BREAKING CHANGES (login core)
348
+
349
+ * **E-mail only: WhatsApp and SMS delivery removed.** `POST /api/magic-link/send-link`
350
+ now requires `email`. The fields `phoneNumber`, `phone`, `via` and
351
+ `delivery` values other than `"email"` are rejected with a 400
352
+ `ValidationError` ("Only e-mail delivery is supported ..."). The response no
353
+ longer contains `phoneNumber` and never `delivery: "whatsapp"`; the 503
354
+ `WhatsAppDeliveryError` is gone. Settings keys `whatsapp_*` and `sms_*` are
355
+ deleted on startup, and `otp_type` only accepts `"email"` (stored `sms` or
356
+ `totp` values are rewritten to `email`).
357
+ Migration: send e-mail addresses only. Operators who used the built-in
358
+ WhatsApp transport of releases up to 5.2.x should securely delete the
359
+ `.whatsapp-auth` directory in the Strapi project root.
360
+ * **Reserved context keys are rejected.** `context` on send-link may not
361
+ contain `requiresOTP`, `otpVerified`, `requiresTOTP`, `totpVerified`,
362
+ `mfaVerified`, `totpLogin`, `userId`, `userDocumentId`, `user`,
363
+ `expires_at`, `expiresAt`, `ttl` or JWT claim names (`id`, `iat`, `exp`,
364
+ `nbf`, `iss`, `sub`, `aud`, `jti`, `sessionId`, `type`), at most 20 keys.
365
+ Public context now passes the same `context_whitelist` /
366
+ `context_blacklist` policy as admin-created tokens. Second-factor state is
367
+ stored in the new private token attribute `challenge`; old tokens whose
368
+ context still carries such flags cannot complete a challenge (open the link
369
+ again to start a new one).
370
+ * **E-mail templates no longer execute code.** `message_html`,
371
+ `message_text` and `object` are rendered with a placeholder renderer:
372
+ `{{LOGIN_URL}}`, `{{URL}}`, `{{CODE}}`, `{{EXPIRY_TEXT}}`, `{{EXPIRES_AT}}`,
373
+ `{{EMAIL}}`, `{{USER}}` (values are HTML-escaped in HTML bodies). Legacy
374
+ `<%= VAR %>` placeholders still render and are converted on startup;
375
+ `<%= URL %>?loginToken=<%= CODE %>` becomes `{{LOGIN_URL}}`. Any other
376
+ `<% ... %>` block is printed as text. Unknown placeholders render empty
377
+ instead of failing the send.
378
+ Migration: replace JavaScript expressions in templates with placeholders.
379
+ * **Login links are built by one URL builder.** Precedence:
380
+ `frontend_url` + `frontend_login_path`, then `confirmationUrl`, then
381
+ `<server.url><api.rest.prefix>/magic-link/login`. Existing query strings
382
+ are kept. `confirmationUrl`, `frontend_url` and `callback_url` must be
383
+ `https` URLs (`http` only for localhost); invalid stored values are
384
+ ignored when building links and rejected when saving settings. The old
385
+ default `confirmationUrl` (the bare server URL) is cleared on startup.
386
+ * **`token_length` is 22 to 128** (132+ bits). Stored smaller values are
387
+ raised to 22. Legacy unhashed login tokens are deactivated and no longer
388
+ accepted.
389
+ * **Login responses changed.** The user object is sanitized with Strapi's
390
+ content-API sanitizer (private attributes are no longer returned).
391
+ `expiry_formatted` was removed (format `expires_at` on the client). With
392
+ `use_jwt_token: false` no JWT is minted; the response carries
393
+ `jwtIssued: false` instead of `tokenIssued: true`. The TOTP challenge
394
+ response no longer includes `userId`. Errors use the Strapi envelope
395
+ `{ data: null, error: { status, name, message, details: { key } } }`
396
+ (`error.key` is kept) with translated messages.
397
+ * **JWT lifetime and refresh mode.** In users-permissions `legacy-support`
398
+ mode the JWT now expires after `jwt_token_expires_in` (`15m`, `12h`,
399
+ `30d`, ...). In `jwtManagement: 'refresh'` mode Magic Link logins issue a
400
+ refresh session through the users-permissions session manager and return
401
+ `refreshToken` (or set the refresh cookie when `sessions.httpOnly` or the
402
+ `x-strapi-refresh-cookie: httpOnly` header asks for it), exactly like
403
+ `/api/auth/local`; `context` is only in the response body there.
404
+ Revoking a session in the plugin also revokes the users-permissions
405
+ session, and revocation entries now live until the token expires (not a
406
+ fixed 30 days).
407
+ * **MFA precedence.** A user with an enabled authenticator always gets the
408
+ TOTP challenge after the link click, even when e-mail OTP is enabled
409
+ (previously `optional` mode silently skipped TOTP). In `required` mode a
410
+ user without an authenticator enrols one during the login (see "MFA"
411
+ below).
412
+ * **Auto-registration** needs `allow_magic_links_on_public_registration`,
413
+ `createUserIfNotExists` and users-permissions "Enable sign-ups"
414
+ (`allow_register`). New accounts are created unconfirmed and confirmed on
415
+ their first magic-link login; colliding usernames get a random suffix.
416
+ `username` on send-link may only contain letters, digits, `.`, `_`, `-`.
417
+ TOTP-only login no longer confirms accounts and honours users-permissions
418
+ "email confirmation".
419
+ * **Passwordless compatibility** is served only by the
420
+ `/<api prefix>/passwordless/{send-link,login}` rewrite (now honouring
421
+ `api.rest.prefix`); the duplicate `/api/magic-link/passwordless/*` routes
422
+ and the `compatibility-check` policy were removed. With the setting off,
423
+ the path is passed on instead of answering 404.
424
+
425
+ ### BREAKING CHANGES (account claim)
426
+
427
+ * **Confirming an unconfirmed account clears its password.** When a magic
428
+ link, an e-mail OTP or the TOTP enrolment during login confirms an
429
+ account that was still unconfirmed, the plugin first ends every existing
430
+ session of the account (plugin JWTs and, in refresh mode, all
431
+ users-permissions refresh families), then confirms it with `password`,
432
+ `resetPasswordToken` and `confirmationToken` set to null. This stops
433
+ pre-account hijacking (someone registered the address with their own
434
+ password before the owner). users-permissions refuses password login
435
+ without a password. Migration: users who registered with a password and
436
+ then used a magic link before confirming set a new password through
437
+ "forgot password". Confirmed accounts are not changed.
438
+
439
+ ### Security and fixes (login core)
440
+
441
+ * **Revocation could be bypassed with an upper-case path.** Strapi 5.61
442
+ mounts its routers with @koa/router defaults (`sensitive: false`), so
443
+ `GET /API/users/me` reaches the same route as `/api/users/me`. The
444
+ revocation middleware compared the path case-sensitively and skipped the
445
+ check, so a revoked JWT kept working. The Content API and admin path
446
+ checks now ignore case.
447
+
448
+ * **Refresh mode: revocation ends the whole refresh family.** @strapi/core
449
+ 5.61 rotates a refresh token into a new session id with the same device
450
+ id; revoking only the first id left rotated tokens usable. The plugin now
451
+ revokes by user and device id (`invalidateRefreshToken`), deleting the
452
+ family including rotated rows; other devices stay logged in. Records from
453
+ before the device id was stored can still only revoke the first token.
454
+ * **Refresh mode: access tokens of a revoked family are rejected.** While a
455
+ user has a revoked family (until its maximum lifetime plus one
456
+ access-token lifespan), the revocation middleware checks the
457
+ users-permissions session (`isSessionActive`) of that user's access
458
+ tokens: 401 when it is gone, 503 when the state cannot be read. Other
459
+ users pay no lookup. Admin routes are never checked.
460
+ * **Legacy JWTs without `exp` stay revoked.** Revoking a JWT that never
461
+ expires (users-permissions without `expiresIn`) now blocks it permanently
462
+ instead of for 30 days.
463
+ * **Pending challenges re-check the current MFA policy.** An e-mail OTP no
464
+ longer completes a login after MFA became required or the user enrolled
465
+ an authenticator; re-opening the link starts the required challenge.
466
+ Login-time TOTP enrolment is refused when the user already has an
467
+ active authenticator.
468
+ * `Cache-Control: no-store` is now sent by every login, OTP and TOTP
469
+ endpoint, including `send-link`, `otp/send`, `otp/resend`,
470
+ `totp/status`, `totp/disable` and the admin TOTP status/disable routes.
471
+ * The built-in OTP, authenticator-confirmation and welcome mails are
472
+ translated (de, en, es, fr, pt) from the request's `?locale=` or
473
+ `Accept-Language` (FUN-27); custom login templates are unchanged.
474
+ * Multi-instance (AUTH-14) remains open: the session list, JWT blocklist
475
+ and ban list are plugin-store blobs serialised per process only. See
476
+ "Running behind a proxy or in a cluster" in the README.
477
+
478
+ * `POST /api/magic-link/login { loginToken }` added for frontend landing
479
+ pages, so tokens stay out of access logs and Referer headers; `GET` keeps
480
+ working. Login responses send `Cache-Control: no-store` and
481
+ `Referrer-Policy: no-referrer`.
482
+ * Single-use links are consumed with a conditional update: parallel requests
483
+ with one link yield exactly one session.
484
+ * Re-opening a link with a pending e-mail OTP no longer sends a new code
485
+ while the current one is valid.
486
+ * send-link answers before any lookup or delivery, so neither status nor
487
+ timing reveals whether an account exists; delivery errors are only logged.
488
+ * The JWT revocation check parses `Authorization` like users-permissions
489
+ (any case, any whitespace) and runs only for Content API requests (REST
490
+ under `api.rest.prefix` and the GraphQL endpoint), where it fails closed
491
+ with 503. Admin API requests, including plugin admin routes such as
492
+ `/content-manager/...`, are never checked, so a store problem cannot lock
493
+ administrators out.
494
+ * Plugin logs no longer contain e-mail addresses, URLs with tokens or
495
+ provider errors from the login core.
496
+ * Login mails sent through Magic Mail are `transactional` and carry no
497
+ open-tracking pixel and no tracked links (`enableTracking: false`).
498
+ * Sender falls back to the e-mail plugin `defaultFrom` instead of
499
+ `noreply@example.com`; the default expiry text reads "1 hour".
500
+ * Server error messages are translated (de, en, es, fr, pt); unknown
501
+ `?locale=` values fall back to English.
502
+ * Legacy-mode JWTs carry a random `jti`. Two logins of the same user within
503
+ one second used to get byte-identical tokens, so revoking one session in
504
+ the admin panel also ended the other.
505
+
506
+ ### BREAKING CHANGES (MFA: e-mail OTP, TOTP, backup codes)
507
+
508
+ * **Secrets are mandatory and checked at startup.** `MAGIC_LINK_ENCRYPTION_KEY`
509
+ and `MAGIC_LINK_OTP_PEPPER` must both be set and at least 32 characters
510
+ long, also outside `NODE_ENV=production`. The only exception is
511
+ `MAGIC_LINK_ALLOW_DEV_SECRETS=true` on non-production hosts, which uses a
512
+ development fallback and logs a warning.
513
+ Migration: generate both with
514
+ `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
515
+ If your current encryption key is shorter than 32 characters (or you used
516
+ the development fallback), set the new key in `MAGIC_LINK_ENCRYPTION_KEY`
517
+ and the old value in `MAGIC_LINK_ENCRYPTION_KEY_PREVIOUS` (for the old
518
+ development fallback: the value of `ADMIN_JWT_SECRET`). Startup re-encrypts
519
+ stored TOTP secrets with the new key; remove the previous key afterwards.
520
+ A new pepper only invalidates pending e-mail codes and backup codes.
521
+ * **Authenticator enrolments are namespaced by principal.** TOTP records now
522
+ belong either to a users-permissions user (`up:<id>`) or to an admin user
523
+ (`admin:<id>`); equal numeric ids no longer share a second factor (security
524
+ review S-01). On startup, records from 5.x are assigned only where
525
+ ownership is provable: the users-permissions user or admin user with that
526
+ id must still have the enrolment e-mail. All other records are disabled.
527
+ Unconfirmed setups and the old unsalted backup codes are dropped.
528
+ Migration: users whose authenticator was disabled enrol again (see below).
529
+ * **End users manage their authenticator themselves.** New Content API
530
+ routes (users-permissions JWT): `GET /api/magic-link/totp/status`,
531
+ `POST /totp/setup` (sends an e-mail code; needs `totpCode` when an
532
+ authenticator is already active), `POST /totp/confirm { totpCode, emailCode }`
533
+ (returns 10 backup codes once), `POST /totp/disable { totpCode | backupCode }`,
534
+ `POST /totp/backup-codes { totpCode }`. The README routes
535
+ `/totp/verify` and `/totp/login` never existed; use `/totp/confirm` and
536
+ `/login-totp`.
537
+ Migration: grant the `totp` actions (status, setup, confirm, disable,
538
+ backupCodes) to the **Authenticated** role in *Settings > Users &
539
+ Permissions > Roles*.
540
+ * **Required MFA enrols during login.** With `mfa_mode: 'required'`, a user
541
+ without an authenticator no longer gets 403 `mfa.setupRequired`. `/login`
542
+ answers `{ requiresTOTPSetup: true, loginToken }`; the frontend calls
543
+ `POST /api/magic-link/totp/enroll { loginToken }` (secret and QR code) and
544
+ `POST /api/magic-link/totp/enroll/confirm { loginToken, totpCode }`, which
545
+ returns the session plus `backupCodes`.
546
+ Migration: handle `requiresTOTPSetup` in the frontend landing page.
547
+ * **TOTP parameters are stored per enrolment.** Algorithm, digits and period
548
+ are saved when a user enrols; changing the settings only affects new
549
+ enrolments. The QR code now carries issuer, algorithm, digits and period.
550
+ Re-enrolment stages the new secret and keeps the current authenticator
551
+ active until the new one is confirmed.
552
+ * **Backup codes work.** `verify-mfa-totp` accepts `{ loginToken, backupCode }`
553
+ instead of `totpCode` (one-time use; the response carries
554
+ `backupCodesRemaining`). Codes are 16 characters (`XXXX-XXXX-XXXX-XXXX`,
555
+ about 78 bits), stored as owner-bound HMACs.
556
+ * **Admin TOTP routes** (`/magic-link/otp/totp/*`) manage only the calling
557
+ admin user's own authenticator. `setup` needs `{ token }` (a current code)
558
+ when one is active, `verify` confirms a pending setup and returns backup
559
+ codes, `disable` and `backup-codes` need `{ token }`.
560
+ * **Unbound e-mail OTP is refused while MFA is active.** With
561
+ `otp_strict_binding: false`, `/otp/send` and `/otp/verify` only work when
562
+ `mfa_mode` is `disabled`.
563
+ * **`/otp/verify` issues sessions like every other login.** The response is
564
+ the common login body (`jwt`, sanitized `user`, filtered `context`,
565
+ `expires_at`, `refreshToken` in refresh mode, `otpVerified: true`); the
566
+ session appears in the JWT sessions list and can be revoked. Invalid
567
+ request bodies answer with Strapi's `ValidationError` instead of
568
+ "Failed to send OTP code".
569
+ * **OTP codes:** the `sms` type and the `phoneNumber` attribute were removed
570
+ from `otp-code`. The admin list (`GET /magic-link/otp/codes`) validates its
571
+ query (`page`, `pageSize` up to 1000, `email`, `type`, `status`), paginates on the
572
+ server, returns `pagination.truncated` and no longer returns code hashes
573
+ or token bindings.
574
+
575
+ ### Security and fixes (MFA)
576
+
577
+ * TOTP uses otplib 13 instead of the unmaintained speakeasy 2.0.0. Existing
578
+ base32 secrets stay valid (RFC 6238 test vectors pass).
579
+ * Every accepted TOTP time step is recorded with a compare-and-set update,
580
+ also during setup confirmation, so a code cannot be used twice.
581
+ * E-mail OTP attempts are counted with a compare-and-set update before the
582
+ comparison, so parallel guesses cannot exceed `otp_max_attempts`. Codes are
583
+ drawn with `crypto.randomInt` (no modulo bias).
584
+ * TOTP-only login limits attempts per IP, per (account, IP) pair and per
585
+ account, so one client cannot lock a victim out, and runs a dummy
586
+ verification for unknown accounts (equal timing).
587
+ * Every second-factor code check (TOTP, backup code, e-mail OTP, enrolment
588
+ confirmation, admin authenticator routes) also counts against a `verify`
589
+ limit of 10 attempts per account and 15 minutes (then blocked for 15
590
+ minutes). It stays on when `rate_limit_enabled` is off, so disabling send
591
+ throttling never opens a brute-force path.
592
+ * OTP e-mails no longer read "0 minutes" for short expiries and the plugin no
593
+ longer logs e-mail addresses or codes.
594
+ * Deleting an OTP code from the admin works with the numeric id and answers
595
+ 404 when nothing was deleted; the new `GET /magic-link/totp/enrollment-summary`
596
+ (Settings read) returns the number of enrolled end users.
597
+
598
+ ### Admin panel
599
+
600
+ * The Tokens, OTP codes, JWT sessions, IP bans, Settings and License pages
601
+ were rebuilt on the Strapi design system with shared dialogs and theme
602
+ tokens. They have correct dark-mode colours and contrast, a consistent
603
+ spacing scale, labelled controls, keyboard-accessible tabs and pagination,
604
+ and no emoji.
605
+ * Every page follows the new RBAC actions. Read-only roles see disabled or
606
+ hidden controls, and destructive actions use confirmation dialogs instead
607
+ of `window.confirm`.
608
+ * Settings:
609
+ * saves only changed fields and guards unsaved changes;
610
+ * validates URLs, JWT lifetimes and number ranges like the server;
611
+ * no longer shows `login_path`, `callback_url`, `max_login_attempts` or the
612
+ Email Designer compatibility switch;
613
+ * the MFA mode is a single, lock-out safe choice.
614
+ * Admin token creation sends `ttl` as a top-level field and offers "do not
615
+ send e-mail" only with `tokens.issue`.
616
+ * The License page masks the key.
617
+ * Tokens list (browser acceptance at 320 to 1440 px, light and dark):
618
+ * the view, extend and delete buttons in a row work with Enter and Space
619
+ (the design-system table cell cancelled the native key activation), and
620
+ they stay in fixed columns when a token cannot be extended;
621
+ * dialogs return focus to the row, the search field or "New token" when
622
+ the element that opened them is gone (deleted row, closed details or
623
+ create dialog);
624
+ * the misleading "New user" line under every e-mail is gone (tokens have
625
+ no user relation);
626
+ * white text on the header, tab, button and dialog gradients and the status
627
+ badges reach 4.5:1;
628
+ * no orphan stat card, no horizontal table scrolling from 1024 px, no
629
+ clipped header buttons at 320 px, a compact pagination with "Page x of y"
630
+ below 520 px, and 32 px touch targets for the search clear button and the
631
+ page links on phones.
632
+ * OTP codes, JWT sessions and IP bans (browser acceptance at 320 to 1440 px,
633
+ light and dark):
634
+ * the row actions are reachable with the arrow keys again (the select-all
635
+ cell kept the grid focus), and Space and Enter activate them;
636
+ * Restore is disabled with the reason for expired, refresh-mode and
637
+ account-claim sessions instead of failing with an error;
638
+ * the Ban IP dialog and the confirm dialogs return focus to their opener,
639
+ or to the search field when the row is gone;
640
+ * the status badges reach 4.5:1 (the design-system warning and danger
641
+ badges are 3.6:1 and 3.9:1), the table has one frame with the header
642
+ band at full width, rows line up with the filter bar, and checkboxes and
643
+ page links are 24 px (32 px on phones);
644
+ * no horizontal table scrolling from 1024 px, phone cards without a
645
+ second frame, wrapped IPv6 addresses and no sticky hover tint on touch;
646
+ * the IP placeholder uses a documentation address (203.0.113.10).
647
+ * Settings and License (browser acceptance at 320 to 1440 px, light and dark):
648
+ * a refused save highlights and focuses the fields: link URLs outside
649
+ `allowedLinkOrigins` and security fields changed without
650
+ `settings.security` now get a field message instead of only a toast;
651
+ * "Save" and "N fields need attention" move focus to the first invalid
652
+ field also when its section is already open, and focus returns to the
653
+ field (Enter) or the section header after a save or a discard instead of
654
+ falling to the page body;
655
+ * a section with invalid fields shows "Fields to fix" in its header (the
656
+ design-system accordion never rendered the old description text);
657
+ * the template gallery cards fill their grid cells (equal widths);
658
+ * the "Unsaved changes" badge reaches 4.8:1 (design-system warning badge:
659
+ 3.6:1), and the License key buttons 5.4:1 (white frost: 3.6:1);
660
+ * the sign-in mode cards are named by their title and described by their
661
+ text.
662
+ * All texts are translated into English, German (formal), French, Spanish
663
+ and Portuguese, and dates follow the admin locale. A test now checks that
664
+ every message id used by the admin exists in all five locales.
665
+
666
+ ### BREAKING CHANGES (follow-up fixes, 2026-11-07)
667
+
668
+ * **`POST /magic-link/reset-data` needs `settings.security`.** A reset
669
+ deletes every login token, the JWT session list and all IP bans and resets
670
+ link targets, templates, sender and code rules, so `settings.update` alone
671
+ no longer allows it (403, `error.details.key: settings.security.forbidden`).
672
+ The `securitySettingsKept` response field is gone. Migration: grant
673
+ **Settings > Security** to the roles that run resets.
674
+ * **Sender identity is a security setting.** Changing `from_name`,
675
+ `from_email` or `response_email` needs `settings.security` in addition to
676
+ `settings.update`: the login mail carries the login link, so who it claims
677
+ to come from matters. Sending them unchanged is still allowed.
678
+ * **`mfa_mode: "required"` is refused while nobody is enrolled.** The settings
679
+ API answers 400 (`error.details.key: settings.mfa.noEnrolledUsers`,
680
+ `fields: ["mfa_mode"]`) when MFA would become required (also through the
681
+ legacy `mfa_require_totp`) and no end user has an active authenticator.
682
+ Send `acknowledgeNoEnrolledUsers: true` with the update when your landing
683
+ page handles `requiresTOTPSetup`; the flag is never stored.
684
+ * **`max_login_attempts` is removed.** It was validated but never read; the
685
+ login limiter uses `rate_limit_login_max_attempts`. A stored or sent value
686
+ is dropped.
687
+
688
+ ### Security and fixes (follow-up, 2026-11-07)
689
+
690
+ * `send-link` keys its e-mail rate limiter with a hash of the normalised
691
+ address, like the OTP and TOTP paths, so no address is kept in limiter
692
+ memory and case variants share one bucket.
693
+ * Admin: the Tokens, Settings and License pages no longer show "no
694
+ permissions" to a Super Admin after a full page reload. Strapi 5.61
695
+ `useRBAC` could check while the admin permissions were still loading,
696
+ settle with nothing allowed and never re-check; unconditioned grants from
697
+ the loaded admin permissions now count.
698
+ * Admin: arrow keys in the Tokens table no longer get stuck on the
699
+ select-all header cell.
700
+ * Docs: refresh mode works from Strapi 5.24.0; Strapi's own
701
+ `/api/auth/refresh` reads the httpOnly refresh cookie only from 5.33.3.
702
+
703
+ ### Build, dependencies and release
704
+
705
+ * Removed the unused `lodash` and `yup` dependencies.
706
+ * The root `strapi-server.js` and `strapi-admin.js` load `dist/` (they
707
+ pointed at sources that are not published). Removed the dead
708
+ `.npmignore`, `.externals.json`, `.trigger-release` and the unused
709
+ license-tier map `server/src/config/features.js`.
710
+ * ESLint 10 flat config with `@eslint/js` recommended,
711
+ `eslint-plugin-react-hooks` recommended and `eslint-config-prettier`, plus
712
+ Prettier with `lint`, `lint:fix`, `format` and `format:check` scripts.
713
+ * GitHub Actions are pinned to commit SHAs with least-privilege tokens.
714
+ * New pull-request CI on Node 22 and 24 runs lint, format check, tests,
715
+ build, verify and `npm pack --dry-run`.
716
+ * Releases install without lifecycle scripts and publish with npm trusted
717
+ publishing and provenance.
718
+ * `verify:runtime` checks the public routes, the RBAC policy on every admin
719
+ route and that the server bundle only loads declared dependencies.
720
+
721
+ # [5.6.0](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.5.9...v5.6.0) (2026-10-15)
722
+
723
+
724
+ ### Bug Fixes
725
+
726
+ * align CI installs with lockfile settings ([77580a3](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/77580a36f00f7f41d04be953a8d19a0422d14dce))
727
+ * check out documentation before Pages deploy ([114ef6e](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/114ef6e3e5e4d1039d5db89ae791b633e1189793))
728
+
729
+
730
+ ### Features
731
+
732
+ * delegate WhatsApp OTP to Magic WhatsApp ([64fa660](https://github.com/Schero94/strapi-plugin-magic-link-v5/commit/64fa66053478db45785374840f4e9f6236b428c4))
733
+
1
734
  ## [5.5.9](https://github.com/Schero94/strapi-plugin-magic-link-v5/compare/v5.5.8...v5.5.9) (2026-05-03)
2
735
 
3
736