# Edut Secret System - Two-Factor Designation Spec (Hardened) ## Overview The Edut designation flow is a two-factor protocol: 1. Phone verification by SMS (Twilio). 2. Email verification by protocol email (Mailgun). Both factors are bound to one server-generated designation record. This is a pre-launch identity bootstrap used for launch activation continuity. ## User Experience Sequence (Continue Flow) 1. Initial page state: orb, identity text, footer links. 2. First click anywhere: globe spin intensifies, `continue` appears. 3. Click `continue`: minimal phone input appears. 4. User submits phone: page calls `POST /secret/initiate`. 5. API returns status ticket + expiry. UI enters ambient waiting state. 6. User receives SMS and replies `CONFIRM`. 7. Page polls `GET /secret/status` using the status ticket. 8. On `phone_verified`, page attempts `mailto` open. 9. If browser blocks `mailto`, show explicit fallback action: `continue to email`. 10. User sends email request. 11. Mailgun webhook verifies and marks email complete. 12. UI shows `acknowledged · {token}` and stores local marker. Privacy and Terms links bypass flow and navigate normally. ## Architecture ``` Landing page -> POST /secret/initiate -> SMS outbound via Twilio User reply CONFIRM -> POST /twilio/inbound Landing page poll (ticket-bound) -> GET /secret/status phone verified -> mailto compose (auto attempt + fallback button) User sends email -> POST /mailgun/inbound Mailgun reply -> classified confirmation email ``` ## Infrastructure | Service | Domain / Endpoint | Purpose | |---------|-------------------|---------| | Landing UI | `edut.ai`, `edut.dev` | Continue flow + phone capture + mailto bridge | | API | `api.edut.ai/secret/*` | Initiate + status polling | | Twilio | `api.edut.ai/twilio/inbound` | SMS outbound/inbound verification | | Mailgun | `api.edut.ai/mailgun/inbound` + `secret.edut.ai` | Inbound designation email + confirmation reply | | Database | `/var/lib/edut/secrets.db` | Durable designation state | ## State Machine (Deterministic) `pending_phone` -> `phone_verified` -> `pending_email` -> `acknowledged` Additional terminal/side states: - `expired` (ticket or flow timed out) - `abandoned` (no progress within retention window) - `opted_out` (STOP/UNSUBSCRIBE received) - `rate_limited` (temporary) Rules: 1. No backwards transition except administrative recovery event with audit entry. 2. Email verification is blocked unless state is `pending_email`. 3. `acknowledged` is terminal for this flow. ## API Contracts ## 1) Initiate ### `POST /secret/initiate` Request JSON: ```json { "phone": "+12065550123", "origin": "https://edut.ai", "locale": "en" } ``` Behavior: 1. Normalize phone to E.164. 2. Enforce rate limits (IP + phone + rolling windows). 3. Server generates `code` and `auth_token`. 4. Ignore/reject any client-supplied code/token fields. 5. Create designation row in `pending_phone` state. 6. Issue short-lived polling ticket. 7. Send SMS via Twilio. Response: ```json { "status": "sms_sent", "status_ticket": "st_...", "ticket_expires_at": "2026-02-17T07:40:45Z", "display_token": "0217-0730-4548-2" } ``` Notes: - Do not return `auth_token`. - Do not return internal row id. - `display_token` is presentation-only. ## 2) Status ### `GET /secret/status` Auth: - `Authorization: Bearer {status_ticket}` Behavior: 1. Validate ticket existence and expiry. 2. Enforce ticket + IP rate limits. 3. Return minimal status payload. 4. Ticket is reusable during TTL for polling. 5. Ticket is invalidated at terminal state (`acknowledged`, `expired`, `opted_out`) or TTL expiry. Response example: ```json { "status": "phone_verified", "phone_verified": true, "email_verified": false, "mailto": { "enabled": true, "recipient": "0217073045482@secret.edut.ai", "subject": "EDUT-0217073045482", "body": "..." } } ``` Security notes: - No code in URL path. - No phone/email values in response. ## Twilio Webhook ### `POST /twilio/inbound` Required checks: 1. Verify `X-Twilio-Signature` with `TWILIO_AUTH_TOKEN`. 2. Idempotency on `MessageSid`. 3. Normalize `From` number. Accepted commands (case-insensitive): - `CONFIRM` -> verify phone and transition to `pending_email`. - `STOP`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT` -> transition to `opted_out`. - `HELP` -> send support/help response. Processing rules: - `CONFIRM` applies to latest active designation for that phone within validity window. - Return `2xx` only after persistence or deterministic dedupe. ## Mailgun Webhook ### `POST /mailgun/inbound` Required checks: 1. Verify Mailgun signature with `MAILGUN_SIGNING_KEY`. 2. Idempotency on `Message-Id` (or provider event id fallback). 3. Parse code from recipient local-part first (`{code}@secret.edut.ai`). Processing rules: 1. Match designation by code where state is `pending_email`. 2. Record sender and verification timestamp. 3. Set state to `acknowledged`. 4. Send confirmation reply email. ## SMS Content (Compliance-Safe) Use plain-text SMS (no heavy unicode framing): ``` EDUT GOVERNANCE PROTOCOL Designation: {display_token} Reply CONFIRM to proceed. Reply STOP to opt out. HELP for support. ``` Rationale: - Better carrier compatibility. - Clear STOP/HELP semantics. ## Confirmation Email Template ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ EDUT GOVERNANCE PROTOCOL ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ACCESS REGISTRATION CONFIRMED Designation: #{designation_number} Auth Token: {auth_token} Classification: OBSERVER Status: ACKNOWLEDGED Phone: VERIFIED Email: VERIFIED Timestamp: {iso_timestamp} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ This designation is permanent and non-transferable. You will be notified when your access level changes. Do not reply to this message. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Edut LLC. All rights reserved. Development and licensing of deterministic governance systems. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` ## SQLite Schema ### File: `/var/lib/edut/secrets.db` ```sql CREATE TABLE IF NOT EXISTS designations ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, phone TEXT, phone_verified_at DATETIME, email TEXT, email_verified_at DATETIME, recipient TEXT, message_id TEXT, auth_token TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending_phone', status_ticket TEXT, ticket_expires_at DATETIME, created_at DATETIME DEFAULT (datetime('now')), replied_at DATETIME, reply_status TEXT DEFAULT 'pending' ); CREATE INDEX idx_designations_code ON designations(code); CREATE INDEX idx_designations_phone ON designations(phone); CREATE INDEX idx_designations_email ON designations(email); CREATE INDEX idx_designations_status ON designations(status); CREATE INDEX idx_designations_ticket ON designations(status_ticket); CREATE INDEX idx_designations_created ON designations(created_at); CREATE TABLE IF NOT EXISTS twilio_inbound_events ( message_sid TEXT PRIMARY KEY, designation_code TEXT, from_phone TEXT NOT NULL, body TEXT, received_at DATETIME DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS mailgun_inbound_events ( message_id TEXT PRIMARY KEY, designation_code TEXT, sender TEXT, recipient TEXT, received_at DATETIME DEFAULT (datetime('now')) ); ``` ## Nginx Routing ### File: `/etc/nginx/sites-available/api.edut.ai` ```nginx server { listen 80; server_name api.edut.ai; location /secret/ { proxy_pass http://127.0.0.1:3847; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /twilio/inbound { proxy_pass http://127.0.0.1:3847; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /mailgun/inbound { proxy_pass http://127.0.0.1:3847; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## Environment Variables ```bash SECRETS_DB_PATH=/var/lib/edut/secrets.db AUTH_SALT= # Twilio TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_FROM_NUMBER=<+1 toll-free number> # Mailgun MAILGUN_API_KEY= MAILGUN_DOMAIN=secret.edut.ai MAILGUN_SIGNING_KEY= ``` ## Error Handling + Retry Rules - Invalid Twilio signature: `403`. - Invalid Mailgun signature: `403`. - Duplicate inbound event id: idempotent `2xx`. - Transient DB/send failure: `5xx` so provider retries. - Unknown/expired status ticket: `401` or `410`. - Unknown confirmation attempt: `200` with no state mutation. ## Security Controls - Signature verification for Twilio and Mailgun. - Strict rate limits on initiate and status endpoints. - Ticket-bound polling only; no direct code enumeration. - Auth token never exposed via status/initiate API responses. - SMS STOP/HELP compliance enabled. - SQLite file encryption-at-rest where host supports it, plus regular backups. ## Launch Evolution At launch, only fully verified designations (`phone_verified_at` + `email_verified_at`) are eligible for access-level change messaging. - Classification may transition `OBSERVER -> OPERATOR`. - Auth token remains stable. - Activation handoff can attach to this identity envelope. ## Important Boundary This two-factor designation flow is a pre-launch identity bootstrap. It does not replace runtime trust anchors, workspace isolation, or offline license/runtime enforcement in the product runtime.