Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions frontend/src/lib/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,10 @@ export interface paths {
* Classify Demo
* @description Public classify endpoint gated by email only.
*
* Returns a limited result set (5 systems, 3 results each). Records
* the email as a lead.
* Anonymous callers get the 5-system / 3-result anon tier. Callers
* with a valid `dev_session` cookie (i.e., they completed the
* magic-link sign-in) get the broader 10-system / 5-result tier.
* Records the email as a lead either way.
*
* Per-IP rate guard: 20/hour. The cap is well above any legitimate
* interactive use (a user trying ~5 prompts in a session) but tight
Expand Down Expand Up @@ -1060,6 +1062,12 @@ export interface components {
* @default true
*/
demo: boolean;
/**
* Is Logged In
* @description True when the request carried a valid `dev_session` cookie. Logged-in callers get the broader 10-system / 5-result-per-system tier; anonymous callers get the 5-system / 3-result tier.
* @default false
*/
is_logged_in: boolean;
/** Upgrade Cta */
upgrade_cta: string;
/**
Expand Down Expand Up @@ -2444,7 +2452,9 @@ export interface operations {
query?: never;
header?: never;
path?: never;
cookie?: never;
cookie?: {
dev_session?: string | null;
};
};
requestBody: {
content: {
Expand Down
99 changes: 99 additions & 0 deletions tests/test_magic_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,102 @@ def test_no_op_email_client_when_unconfigured(self, capsys):
client = NoopEmailClient()
client.send(to="x@y.com", subject="s", html="<p>hi</p>", text="hi")
# No exception is the contract; output is informational.


def _fake_http_response(body_bytes):
"""Minimal context-manager stand-in for urllib's urlopen result."""

class _Resp:
def __enter__(self):
return self

def __exit__(self, *exc):
return False

def read(self):
return body_bytes

return _Resp()


class TestResendClient:
"""ResendClient posts a single request to api.resend.com. These
tests mock the HTTP layer, so no API key and no network are
needed."""

def test_send_posts_expected_resend_payload(self, monkeypatch):
import json

from world_of_taxonomy.auth.email import ResendClient

captured = {}

def fake_urlopen(req, timeout=None):
captured["url"] = req.full_url
captured["method"] = req.get_method()
captured["body"] = json.loads(req.data.decode("utf-8"))
return _fake_http_response(b'{"id":"abc-123"}')

monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)

ResendClient(api_key="re_test", sender="noreply@aixcelerator.ai").send(
to="dev@acme.com",
subject="Sign in to WorldOfTaxonomy",
html="<p>link</p>",
text="link",
)

assert captured["url"] == "https://api.resend.com/emails"
assert captured["method"] == "POST"
body = captured["body"]
assert body["from"] == "noreply@aixcelerator.ai"
assert body["to"] == ["dev@acme.com"]
assert body["subject"] == "Sign in to WorldOfTaxonomy"

def test_send_sets_explicit_user_agent(self, monkeypatch):
"""api.resend.com sits behind Cloudflare, which blocks urllib's
default `Python-urllib/x.y` User-Agent with HTTP 403 ("error
code: 1010"). The client must send an explicit, non-default UA
or every magic-link email is silently dropped at the edge."""
from world_of_taxonomy.auth.email import ResendClient

captured = {}

def fake_urlopen(req, timeout=None):
# urllib normalises header keys to "User-agent".
captured["ua"] = req.get_header("User-agent")
return _fake_http_response(b'{"id":"abc-123"}')

monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)

ResendClient(api_key="re_test").send(
to="x@y.com", subject="s", html="<p>h</p>", text="h",
)
assert captured["ua"]
assert "python-urllib" not in captured["ua"].lower()

def test_send_does_not_raise_on_http_error(self, monkeypatch, caplog):
"""A 403 / bad key must be swallowed and logged with the response
body: signup cannot 500 on an email-infrastructure failure, and
the body is what distinguishes an API error from a Cloudflare
edge block."""
import io
import urllib.error

from world_of_taxonomy.auth.email import ResendClient

def fake_urlopen(req, timeout=None):
raise urllib.error.HTTPError(
req.full_url, 403, "Forbidden", {},
io.BytesIO(b"error code: 1010"),
)

monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)

with caplog.at_level("ERROR", logger="wot.auth.email"):
ResendClient(api_key="re_bad").send(
to="x@y.com", subject="s", html="<p>h</p>", text="h",
)
messages = [r.getMessage() for r in caplog.records]
assert any("resend_send_failed" in m for m in messages)
assert any("1010" in m for m in messages)
24 changes: 18 additions & 6 deletions world_of_taxonomy/auth/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ def __init__(self, api_key: str, sender: str = "noreply@aixcelerator.ai"):
self._sender = sender

def send(self, *, to: str, subject: str, html: str, text: str) -> None:
import urllib.request
import json
import urllib.error
import urllib.request

req = urllib.request.Request(
"https://api.resend.com/emails",
Expand All @@ -61,16 +62,27 @@ def send(self, *, to: str, subject: str, html: str, text: str) -> None:
headers={
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
# An explicit User-Agent is required: api.resend.com sits
# behind Cloudflare, which blocks urllib's default
# `Python-urllib/x.y` UA with HTTP 403 ("error code:
# 1010") before the request ever reaches Resend's API -
# silently dropping every magic-link email.
"User-Agent": "WorldOfTaxonomy/1.0",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as response:
if response.status >= 400:
logger.error(
"resend_send_failed: status=%s body=%s",
response.status, response.read()[:500],
)
response.read()
except urllib.error.HTTPError as exc:
# Resend returns a JSON error envelope on 4xx/5xx. Capturing
# the body matters: a bare "HTTP Error 403" hides whether the
# cause is the API (bad key / unverified domain) or the
# Cloudflare edge (blocked User-Agent -> "error code: 1010").
detail = exc.read()[:500] if hasattr(exc, "read") else b""
logger.error(
"resend_send_failed: status=%s body=%s", exc.code, detail,
)
except Exception as exc:
logger.exception("resend_send_exception: %s", exc)

Expand Down
Loading