333 lines
8.8 KiB
Markdown
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.
|