{"record":{"id":"689400da3ad5fab3","repo":"usestrix/strix","slug":"work-email-required","errorCode":"work_email_required","errorMessage":"work_email_required","messagePattern":"work_email_required","errorType":"error_code","errorClass":"RelayError","httpStatus":400,"severity":"warning","filePath":"strix/interface/viewer/auth.py","lineNumber":176,"sourceCode":"    try:\n        data = json.loads(raw or b\"{}\")\n    except json.JSONDecodeError:\n        return {}\n    return data if isinstance(data, dict) else {}\n\n\ndef otp_start(email: str) -> None:\n    \"\"\"Ask the relay to email a verification code. Raises RelayError on failure.\"\"\"\n    status, data = _post_json(\"/api/oss/otp/start\", {\"email\": email}, timeout=_OTP_TIMEOUT)\n    if status == 200:\n        return\n    if status == 429:\n        raise RelayError(\"rate_limited\")\n    if status == 400:\n        # The relay uses 400 both for a malformed address and, separately, to\n        # reject a free/personal email domain (it wants a work email).\n        if data.get(\"error\") == \"work_email_required\":\n            raise RelayError(\"work_email_required\")\n        raise RelayError(\"invalid_email\")\n    raise RelayError(\"unavailable\")\n\n\ndef otp_verify(email: str, code: str) -> dict[str, Any]:\n    \"\"\"Verify a code. Returns ``{token, email, expires_at}`` or raises RelayError.\"\"\"\n    status, data = _post_json(\n        \"/api/oss/otp/verify\",\n        {\"email\": email, \"code\": code},\n        timeout=_OTP_TIMEOUT,\n    )\n    if status == 200 and isinstance(data.get(\"token\"), str):\n        # A token with no usable expiry cannot unlock history locally (the gate\n        # fails closed), so treat such a response as a failed verification rather\n        # than reporting success and then leaving the user stuck unverified.\n        if parse_expiry(data.get(\"expires_at\")) is None:\n            raise RelayError(\"unavailable\")\n        return data","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/viewer/auth.py#L158-L194","documentation":"RelayError('work_email_required') raised by otp_start() when POST /api/oss/otp/start returns 400 with body error='work_email_required'. The relay deliberately rejects free/personal email domains (gmail.com, outlook.com, etc.) — it wants a work address — and this code distinguishes that policy rejection from a malformed address.","triggerScenarios":"Calling otp_start('user@gmail.com') or any address on a consumer/freemail domain. The relay returns 400 with {\"error\": \"work_email_required\"} and the client maps it to this specific RelayError code.","commonSituations":"Individual researchers or hobbyists who only have personal email accounts trying to unlock viewer history/report features; company emails that route through a gmail frontend (domains verified as freemail by the relay's domain list).","solutions":["Use a corporate/work domain email address for verification","If you only have a personal address, use the viewer without verification — core local viewing works; only relay-backed extras (history unlock, report delivery) need it"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"FREE_DOMAINS = {\"gmail.com\", \"outlook.com\", \"hotmail.com\", \"yahoo.com\", \"icloud.com\"}\n\ndef is_work_email(email: str) -> bool:\n    domain = email.rsplit(\"@\", 1)[-1].lower()\n    return \"@\" in email and domain not in FREE_DOMAINS","typeGuard":null,"tryCatchPattern":"try:\n    otp_start(email)\nexcept RelayError as e:\n    if e.code == \"work_email_required\":\n        show(\"Use a work email address; personal domains are not accepted.\")\n    else:\n        raise","preventionTips":["Collect a corporate-domain email up front in verification UIs","Explain the work-email requirement in the form's help text","Fall back to unverified local usage if no work email exists"],"tags":["strix","relay","viewer","auth","email-policy"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}