365 lines
10 KiB
Markdown
365 lines
10 KiB
Markdown
# 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=<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: `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.
|