10 KiB
Edut Secret System - Two-Factor Designation Spec (Hardened)
Overview
The Edut designation flow is a two-factor protocol:
- Phone verification by SMS (Twilio).
- 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)
- Initial page state: orb, identity text, footer links.
- First click anywhere: globe spin intensifies,
continueappears. - Click
continue: minimal phone input appears. - User submits phone: page calls
POST /secret/initiate. - API returns status ticket + expiry. UI enters ambient waiting state.
- User receives SMS and replies
CONFIRM. - Page polls
GET /secret/statususing the status ticket. - On
phone_verified, page attemptsmailtoopen. - If browser blocks
mailto, show explicit fallback action:continue to email. - User sends email request.
- Mailgun webhook verifies and marks email complete.
- 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:
- No backwards transition except administrative recovery event with audit entry.
- Email verification is blocked unless state is
pending_email. acknowledgedis terminal for this flow.
API Contracts
1) Initiate
POST /secret/initiate
Request JSON:
{
"phone": "+12065550123",
"origin": "https://edut.ai",
"locale": "en"
}
Behavior:
- Normalize phone to E.164.
- Enforce rate limits (IP + phone + rolling windows).
- Server generates
codeandauth_token. - Ignore/reject any client-supplied code/token fields.
- Create designation row in
pending_phonestate. - Issue short-lived polling ticket.
- Send SMS via Twilio.
Response:
{
"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_tokenis presentation-only.
2) Status
GET /secret/status
Auth:
Authorization: Bearer {status_ticket}
Behavior:
- Validate ticket existence and expiry.
- Enforce ticket + IP rate limits.
- Return minimal status payload.
- Ticket is reusable during TTL for polling.
- Ticket is invalidated at terminal state (
acknowledged,expired,opted_out) or TTL expiry.
Response example:
{
"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:
- Verify
X-Twilio-SignaturewithTWILIO_AUTH_TOKEN. - Idempotency on
MessageSid. - Normalize
Fromnumber.
Accepted commands (case-insensitive):
CONFIRM-> verify phone and transition topending_email.STOP,UNSUBSCRIBE,CANCEL,END,QUIT-> transition toopted_out.HELP-> send support/help response.
Processing rules:
CONFIRMapplies to latest active designation for that phone within validity window.- Return
2xxonly after persistence or deterministic dedupe.
Mailgun Webhook
POST /mailgun/inbound
Required checks:
- Verify Mailgun signature with
MAILGUN_SIGNING_KEY. - Idempotency on
Message-Id(or provider event id fallback). - Parse code from recipient local-part first (
{code}@secret.edut.ai).
Processing rules:
- Match designation by code where state is
pending_email. - Record sender and verification timestamp.
- Set state to
acknowledged. - 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
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
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
SECRETS_DB_PATH=/var/lib/edut/secrets.db
AUTH_SALT=<random 32+ char>
# Twilio
TWILIO_ACCOUNT_SID=<sid>
TWILIO_AUTH_TOKEN=<auth token>
TWILIO_FROM_NUMBER=<+1 toll-free number>
# Mailgun
MAILGUN_API_KEY=<sending api key>
MAILGUN_DOMAIN=secret.edut.ai
MAILGUN_SIGNING_KEY=<webhook signing key>
Error Handling + Retry Rules
- Invalid Twilio signature:
403. - Invalid Mailgun signature:
403. - Duplicate inbound event id: idempotent
2xx. - Transient DB/send failure:
5xxso provider retries. - Unknown/expired status ticket:
401or410. - Unknown confirmation attempt:
200with 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.