web/docs/secret-system-spec.md

10 KiB

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:

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

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

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

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

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