web/docs/secret-system-spec.md

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.