web/docs/secret-system-spec.md

8.8 KiB

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:

{
  "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:
{
  "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:

{
  "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

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

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 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.