web/docs/secret-system-spec.md

333 lines
8.8 KiB
Markdown

# Edut Secret System - Two-Factor Designation Spec
## Overview
The Edut designation flow is a two-factor protocol:
1. Phone verification by SMS (Twilio).
2. Email verification by classified mailto request (Mailgun).
Both factors bind to the same designation code and auth token. This is not a throwaway waitlist. It is the pre-launch identity envelope that can transition into launch activation workflows.
## End-to-End Flow
```
Visitor clicks orb on edut.ai
-> phone input appears
-> user submits phone
POST /secret/initiate
-> code issued
-> row created/updated in SQLite
-> SMS sent via Twilio
User receives SMS and replies: CONFIRM
Twilio POST /twilio/inbound
-> signature verified
-> pending designation matched
-> phone_verified_at set
Landing page polls /secret/status/{code}
-> sees phone_verified=true
-> opens mailto:{code}@secret.edut.ai with protocol body
User sends email
Mailgun POST /mailgun/inbound
-> signature verified
-> code parsed from recipient local-part
-> email_verified_at set
-> classified confirmation reply sent
Landing page stores acknowledged token
```
## Infrastructure
| Service | Domain / System | Purpose |
|---------|------------------|---------|
| Landing UI | `edut.ai`, `edut.dev` | Orb, phone capture, mailto initiation |
| Twilio | Toll-free SMS number | Outbound designation SMS + inbound CONFIRM replies |
| Mailgun | `secret.edut.ai` | Inbound email processing + confirmation replies |
| API host | `api.edut.ai` | `/secret/*`, `/twilio/inbound`, `/mailgun/inbound` |
| Database | SQLite (`/var/lib/edut/secrets.db`) | Durable designation state |
## Twilio Channel Design
### Number strategy
- Use a toll-free SMS number.
- Voice is disabled or rejected.
- Complete toll-free verification before production traffic.
### Outbound SMS template
```
━━━━━━━━━━━━━━━━━
EDUT GOVERNANCE PROTOCOL
Designation: {token}
Reply CONFIRM to proceed.
━━━━━━━━━━━━━━━━━
```
Where `{token}` is formatted from the code:
- code: `0217073045482`
- token: `0217-0730-4548-2`
## API Surface
## 1) Initiate
### `POST /secret/initiate`
Request JSON:
```json
{
"phone": "+12065550123",
"origin": "https://edut.ai"
}
```
Behavior:
1. Normalize phone to E.164.
2. Rate-limit by IP, phone, and rolling time window.
3. Generate designation code (`MMDDHHmmssmmm`) and auth token.
4. Insert or upsert designation row with `phone` and `created_at`.
5. Send SMS via Twilio.
6. Return `200` with minimal response:
```json
{
"code": "0217073045482",
"token": "0217-0730-4548-2",
"status": "sms_sent"
}
```
Notes:
- Do not expose internal row id.
- Never return auth token in API responses.
## 2) Status
### `GET /secret/status/{code}`
Response JSON:
```json
{
"code": "0217073045482",
"phone_verified": true,
"email_verified": false,
"status": "phone_verified"
}
```
Rules:
- Polling endpoint is read-only.
- Response is minimal and does not expose phone/email values.
- Apply rate limits to prevent enumeration.
## 3) Twilio inbound
### `POST /twilio/inbound`
Expected Twilio fields include:
- `From` (sender phone)
- `To` (Edut toll-free number)
- `Body` (message body)
- `MessageSid`
Verification:
- Validate `X-Twilio-Signature` against Twilio auth token.
- Reject unsigned or invalid payloads.
Processing:
1. Normalize sender phone.
2. Accept `CONFIRM` (case-insensitive, trim whitespace).
3. Find latest pending designation for that phone within validity window.
4. Idempotency key: `MessageSid`.
5. Mark `phone_verified_at` and set state `phone_verified`.
6. Return `2xx` only after persistence.
## 4) Mailgun inbound
### `POST /mailgun/inbound`
Expected Mailgun fields include:
- `recipient` (`{code}@secret.edut.ai`)
- `sender`
- `Message-Id`
- `subject`
Verification:
- Verify Mailgun webhook signature using Mailgun signing key.
Processing:
1. Parse `code` from recipient local-part.
2. Require existing designation row with `phone_verified_at IS NOT NULL`.
3. Record `email`, `message_id`, and `email_verified_at`.
4. Idempotency key: `Message-Id` (or Mailgun event id fallback).
5. Send confirmation reply through Mailgun.
## 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',
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_created ON designations(created_at);
CREATE TABLE IF NOT EXISTS twilio_inbound_events (
message_sid TEXT PRIMARY KEY,
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,
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 and Idempotency
- Twilio signature failure: return `403`.
- Mailgun signature failure: return `403`.
- Duplicate inbound event id: treat as idempotent success.
- Transient DB/send failure: return `5xx` so provider retries.
- Unknown or expired confirmation attempts: return `200` and no state mutation.
## Security Controls
- Signature verification on both inbound providers.
- Per-IP and per-phone initiate throttles.
- Per-code polling throttles.
- Do not expose auth token except in confirmation email.
- Back up SQLite and protect file permissions.
## Launch Evolution
At launch, each fully verified designation (`phone_verified_at` + `email_verified_at`) is eligible for activation messaging:
- Classification can transition from `OBSERVER` to `OPERATOR`.
- Auth token remains stable.
- Message can include deployment handoff link or activation instructions.
## Important Boundary
This two-factor designation flow is a pre-launch identity bootstrap. It does not replace runtime device trust anchors, workspace isolation controls, or offline license enforcement in the product runtime.