Magic link
Official plugin — request and verify a one-time email link to sign in under /auth.
magicLink() issues a hashed, single-use token (default 10 minutes). Request sends the link
via Channel (auth-magic-link); verify exchanges it for a hybrid session and creates the user
on first success.
The one rule
Enable gate.auth, then .plug(magicLink()). Tokens are hashed at rest — never log the raw link.
Delivery goes through fx.send; use exposeDevToken only for local DX without SMTP.
Quick start
Plug it
import { oke } from "okengine";
import { magicLink } from "okengine/plugins";
export const app = oke({
name: "shop",
env: "dev",
gate: { auth: {} },
}).plug(magicLink({ baseUrl: "http://127.0.0.1:6530" }));Request a link
const { data } = await api.auth.requestMagicLink({ email: "ali@example.com" });
// data.ok === true; Channel delivers auth-magic-link
// data.devToken only when exposeDevTokenPOST /auth/magic-link/request. Under oke test the console driver captures mail; with
oke dev, Mailpit receives the real SMTP message.
Verify
const { data } = await api.auth.verifyMagicLink({ token });
// session tokens + userIdPOST /auth/magic-link/verify. Bad or reused tokens → AuthFailed /
invalid_credentials.
Pre-account hijack defense
Completing verify proves inbox ownership. An unverified email+password account parked on that
address is reclaimed (sessions revoked, hashes cleared, emailVerified set). Verified owners
re-auth only — no credential purge.
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
secret | string | active* | HMAC secret (*from gate.auth) |
sessions | SessionStore | active* | Session store |
ttlMs | number | 10m | Challenge lifetime |
baseUrl | string | OKE_APP_URL or :6530 | Origin used to build the magic link |
from | string | OKE <no-reply@oke.local> | Template From address |
exposeDevToken | boolean | false | Include raw token in the request response |
identities | IdentityStore | new | Email → user map |
verifications | VerificationStore | new | Challenge store |
Surfaces
| Flow | Path | Gate |
|---|---|---|
auth.requestMagicLink | POST /auth/magic-link/request | gate.public + otp rate |
auth.verifyMagicLink | POST /auth/magic-link/verify | gate.public + otp rate |
Consequence: the plugin contributes the auth-magic-link Channel template and EN/AR
catalog bodies ({{link}}, {{token}}). Override copy by merging your own catalog at boot.
Delivery drivers
drivers.channel.email | Delivery |
|---|---|
console | Dev inbox (local/test default) |
smtp | Any SMTP host — Mailpit under oke dev (dev default) |
resend / sndr | Hosted email APIs |
taqnyat-mail | Taqnyat Mail API (additive option) |
Taqnyat Mail
export default {
drivers: {
channel: {
email: { dev: "smtp", test: "console", prod: "taqnyat-mail" },
},
},
};| Env | Meaning |
|---|---|
TAQNYAT_MAIL_TOKEN | Taqnyat bearer token enabled for Email |
TAQNYAT_CAMPAIGN | Campaign name required by Taqnyat mailSend.php |
The plugin needs no change — delivery stays Channel-mediated via fx.send. SMTP/Mailpit
remains the default docker path; taqnyat-mail is strictly additive.
Live tests (opt-in)
The Taqnyat live suite sends real email and burns real quota, so it is double-gated: it runs
only when OKE_EMAIL_LIVE=1 and the real credentials (TAQNYAT_MAIL_TOKEN,
TAQNYAT_CAMPAIGN, plus OKE_TEST_TAQNYAT_MAIL) are all present.
Credentials alone never send; without the flag the suite skips visibly — never a silent pass.
OKE_EMAIL_LIVE=1 TAQNYAT_MAIL_TOKEN=… TAQNYAT_CAMPAIGN=auth \
OKE_TEST_TAQNYAT_MAIL=you@example.com bun test src/pluginsTroubleshooting
Token expired (default 10m), already used, or mistyped. Request a new link.
In local / test the console driver captures mail into the inbox — nothing hits a mailbox.
Run oke dev and open Mailpit (MAILPIT_UI_URL) to see the rendered message. For
unit tests without SMTP, set exposeDevToken: true.