diff --git a/docs/secret-system-spec.md b/docs/secret-system-spec.md index eb2b374..df93b2a 100644 --- a/docs/secret-system-spec.md +++ b/docs/secret-system-spec.md @@ -1,158 +1,182 @@ -# Edut Secret System — Deployment Spec +# Edut Secret System - Two-Factor Designation Spec ## Overview -The Edut secret system is an organic waitlist that runs on `edut.ai`. Visitors click the landing page, their email client opens with a pre-filled classified-looking message, they send it, and an auto-reply confirms their designation. No forms, no signups — they email you, you reply. +The Edut designation flow is a two-factor protocol: ---- +1. Phone verification by SMS (Twilio). +2. Email verification by classified mailto request (Mailgun). -## Architecture +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 - ↓ -Email client opens: mailto:0217073045482@secret.edut.ai - ↓ -Person sends email - ↓ -Mailgun receives on catch-all (*@secret.edut.ai) - ↓ -Mailgun POSTs to https://api.edut.ai/mailgun/inbound - ↓ -Webhook parses email, stores in SQLite, sends classified reply - ↓ -Reply threads under original email in sender's inbox - ↓ -Copy of inbound + reply also forwarded to j@edut.ai (Gmail archive) + -> 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 -## Infrastructure (Already Configured) +| 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 | -| Service | Domain | Purpose | -|---------|--------|---------| -| Google Workspace | edut.ai | Business email (j@edut.ai), OAuth | -| Mailgun (Edut account) | secret.edut.ai | Catch-all inbound, API sending | -| Cloudflare | api.edut.ai → 89.167.8.148 | Webhook endpoint | -| Hetzner | 89.167.8.148 | Server running webhook + nginx | +## Twilio Channel Design -### Mailgun Configuration -- Domain: `secret.edut.ai` (verified, green) -- Wildcard: **On** -- Tracking (click/open/unsubscribe): **All off** -- Sending API key: stored securely (ask Joshua) -- Inbound route: `match_recipient(".*@secret.edut.ai")` → `https://api.edut.ai/mailgun/inbound` (Forward + Stop, priority 0) -- Route also forwards to `j@edut.ai` for Gmail archive +### Number strategy ---- +- Use a toll-free SMS number. +- Voice is disabled or rejected. +- Complete toll-free verification before production traffic. -## Component 1: Nginx Configuration +### Outbound SMS template -### File: `/etc/nginx/sites-available/api.edut.ai` +``` +━━━━━━━━━━━━━━━━━ +EDUT GOVERNANCE PROTOCOL -```nginx -server { - listen 80; - server_name api.edut.ai; +Designation: {token} +Reply CONFIRM to proceed. +━━━━━━━━━━━━━━━━━ +``` - 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; - } +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" } ``` -SSL is handled by Cloudflare (proxied). Nginx listens on 80, Cloudflare terminates TLS. +Behavior: -Port `3847` is arbitrary — the webhook service listens here. +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: ---- - -## Component 2: Webhook Service - -### Language -Go or Python. Single file, minimal dependencies. Must be production-ready from first deploy. - -### Endpoint: `POST /mailgun/inbound` - -Mailgun sends a multipart form POST with these fields (among others): - -| Field | Description | -|-------|-------------| -| `sender` | Email address of the person who sent the email | -| `recipient` | The timestamp address (e.g., `0217073045482@secret.edut.ai`) | -| `subject` | `EDUT-0217073045482` | -| `body-plain` | Plain text body (the classified access request) | -| `Message-Id` | Original message ID (needed for threading reply) | -| `Date` | Email date header | - -### Processing Logic - -``` -1. Parse the recipient local-part to extract the code - - Recipient format: "{code}@secret.edut.ai" where code is MMDDHHmmssmmm (13 digits) - - Extract code from `recipient` first; use subject parsing only as fallback validation - -2. Parse sender email address - -3. Store in SQLite: - - id (auto-increment — this is their sequential designation number) - - code (the timestamp code from subject) - - email (sender address) - - recipient (the full recipient address) - - message_id (Message-Id header for threading) - - created_at (server timestamp) - -4. Format the token for display: - - code "0217073045482" → token "0217-0730-4548-2" - - Slice: [0:4]-[4:8]-[8:12]-[12:] - -5. Generate auth token: - - Hash the code with a secret salt → first 16 chars of hex digest - - Format as: xxxx-xxxx-xxxx-xxxx - -6. Get the sequential designation number (the SQLite auto-increment id) - - Pad to 4 digits: id 47 → "0047" - -7. Build the classified reply (see template below) - -8. Send reply via Mailgun API: - - From: "EDUT " - - To: sender's email - - Subject: "Re: EDUT-{code}" - - In-Reply-To: original Message-Id header - - References: original Message-Id header - - Body: plain text classified reply - - Use the sending API key for secret.edut.ai domain +```json +{ + "code": "0217073045482", + "token": "0217-0730-4548-2", + "status": "sms_sent" +} ``` -### Mailgun Signature Verification +Notes: -Every inbound POST from Mailgun includes a signature. **Verify it** to prevent spoofing: +- Do not expose internal row id. +- Never return auth token in API responses. -- `timestamp` — Unix timestamp -- `token` — Random string -- `signature` — HMAC-SHA256 of `timestamp + token` using your Mailgun webhook signing key +## 2) Status -Reject any request that fails verification. +### `GET /secret/status/{code}` -### Error Handling +Response JSON: -- If recipient/subject does not match expected format, return `200` and drop as noise. -- Use `Message-Id` (or Mailgun event id) as idempotency key for inbound processing. -- If SQLite write fails due to transient failure, return `5xx` so Mailgun retries. -- If Mailgun send fails, retry with bounded backoff; if still failing, return `5xx`. -- Return `2xx` only after persistence succeeds and outbound reply is accepted or deterministically deduped. +```json +{ + "code": "0217073045482", + "phone_verified": true, + "email_verified": false, + "status": "phone_verified" +} +``` ---- +Rules: -## Component 3: Auto-Reply Email Template +- Polling endpoint is read-only. +- Response is minimal and does not expose phone/email values. +- Apply rate limits to prevent enumeration. -### Plain text body: +## 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 ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -166,6 +190,8 @@ ACCESS REGISTRATION CONFIRMED Classification: OBSERVER Status: ACKNOWLEDGED + Phone: VERIFIED + Email: VERIFIED Timestamp: {iso_timestamp} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -183,182 +209,124 @@ deterministic governance systems. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` -### Variable Substitutions - -| Variable | Source | Example | -|----------|--------|---------| -| `{designation_number}` | SQLite auto-increment id, zero-padded to 4 | `0047` | -| `{auth_token}` | HMAC-SHA256 hash of code, formatted xxxx-xxxx-xxxx-xxxx | `e7d2-4f1a-9bc3-a210` | -| `{iso_timestamp}` | Server UTC time in ISO 8601 | `2026-02-17T07:30:45Z` | - -### Email Headers - -``` -From: EDUT -To: {sender_email} -Subject: Re: EDUT-{code} -In-Reply-To: {original_message_id} -References: {original_message_id} -Content-Type: text/plain; charset=utf-8 -``` - -The `In-Reply-To` and `References` headers ensure the reply threads under the original email in Gmail/Outlook/Apple Mail. - ---- - -## Component 4: Landing Page Deployment - -### Files -- `public/index.html` — the Three.js globe landing page -- `public/privacy/index.html` — privacy policy -- `public/terms/index.html` — terms of use -- `translations/*.json` — locale bundles for localized landing content - -### Deploy to both domains: -- `edut.ai` — primary landing page -- `edut.dev` — same page for now - -### Nginx for landing pages: - -```nginx -server { - listen 80; - server_name edut.ai www.edut.ai; - root /var/www/edut.ai; - index index.html; -} - -server { - listen 80; - server_name edut.dev www.edut.dev; - root /var/www/edut.dev; - index index.html; -} -``` - -Copy `public/index.html` to `/var/www/edut.ai/index.html` and `/var/www/edut.dev/index.html`. -Serve `translations/` at `/translations` so locale files can be loaded by the landing page. - ---- - -## Component 5: SQLite Database +## SQLite Schema ### File: `/var/lib/edut/secrets.db` -### Schema: - ```sql CREATE TABLE IF NOT EXISTS designations ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, - email TEXT NOT NULL, - recipient TEXT NOT NULL, + 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_email ON designations(email); 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 -## Component 6: Launch Day Email +### File: `/etc/nginx/sites-available/api.edut.ai` -When the platform launches, query all designations and send the access level change email through Mailgun: +```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; + } +} ``` -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -EDUT GOVERNANCE PROTOCOL -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -ACCESS LEVEL CHANGE - - Designation: #{designation_number} - Auth Token: {auth_token} - Classification: OPERATOR - Status: ACTIVE - - Your system is ready for deployment. - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Edut LLC. All rights reserved. -Development and licensing of -deterministic governance systems. -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -``` - -Same format as the access confirmation. Same auth token. Classification changes from OBSERVER to OPERATOR. Status changes from ACKNOWLEDGED to ACTIVE. - ---- ## Environment Variables ```bash -MAILGUN_API_KEY= -MAILGUN_DOMAIN=secret.edut.ai -MAILGUN_SIGNING_KEY= SECRETS_DB_PATH=/var/lib/edut/secrets.db -AUTH_SALT= +AUTH_SALT= + +# Twilio +TWILIO_ACCOUNT_SID= +TWILIO_AUTH_TOKEN= +TWILIO_FROM_NUMBER=<+1 toll-free number> + +# Mailgun +MAILGUN_API_KEY= +MAILGUN_DOMAIN=secret.edut.ai +MAILGUN_SIGNING_KEY= ``` ---- +## Error Handling and Idempotency -## Systemd Service +- 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. -### File: `/etc/systemd/system/edut-secret.service` +## Security Controls -```ini -[Unit] -Description=Edut Secret Webhook -After=network.target +- 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. -[Service] -Type=simple -User=www-data -WorkingDirectory=/opt/edut/secret -ExecStart=/opt/edut/secret/secret-server -EnvironmentFile=/opt/edut/secret/.env -Restart=always -RestartSec=5 +## Launch Evolution -[Install] -WantedBy=multi-user.target -``` +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. -## Testing +## Important Boundary -1. Send an email to `test0217120000000@secret.edut.ai` -2. Verify webhook receives the POST -3. Verify SQLite entry created with designation #1 -4. Verify classified auto-reply received and threads correctly -5. Verify copy arrives in `j@edut.ai` Gmail -6. Send a second email to verify designation number increments to #2 -7. Clear test data before going live - ---- - -## Security Notes - -- Verify Mailgun webhook signatures on every request -- Rate limit the endpoint (prevent abuse if someone discovers the URL) -- The auth token is a one-way hash — it cannot be reversed to reveal the code or email -- SQLite file should be backed up regularly (it's the designation list) -- The sending API key must never be exposed in code or logs - ---- - -## Domain Separation - -| Domain | Purpose | -|--------|---------| -| edut.ai | Brand, landing page, business email, public identity | -| secret.edut.ai | Catch-all email for secret system (Mailgun) | -| api.edut.ai | Webhook endpoint (Hetzner via Cloudflare) | -| edut.dev | Product, developer docs, technical infrastructure | +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. diff --git a/docs/vision.md b/docs/vision.md index 7beb6f1..bbb6236 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -10,15 +10,18 @@ The site should feel precise and established while avoiding disclosure of private implementation IP. -## Primary Experience +## Primary Experience (Two-Factor Designation) 1. A visitor lands on `edut.ai`. 2. They see only the globe, identity line, and meaning line. 3. They click anywhere. -4. The globe accelerates and a pre-filled protocol-style email opens. -5. The recipient is a timestamp address (`{timestamp}@secret.edut.ai`). -6. They send it and receive a designation confirmation with auth token. -7. At launch, the same designation receives a follow-up changing classification from `OBSERVER` to `OPERATOR` with status `ACTIVE`. +4. A minimal phone field appears inline (same aesthetic language). +5. They enter their number and submit. +6. They receive SMS from Edut protocol channel with designation token and `CONFIRM` instruction. +7. They reply `CONFIRM`. +8. Once phone verification is detected, the page opens the classified mailto request for the same designation code. +9. They send the email. +10. They receive confirmation showing designation + auth token with both channels verified. The interaction is intended to feel like protocol registration, not a marketing funnel. @@ -48,7 +51,7 @@ The interaction is intended to feel like protocol registration, not a marketing - `edut.ai`: primary public surface and business identity. - `edut.dev`: developer-facing domain (same landing for now). - `secret.edut.ai`: inbound designation email namespace. -- `api.edut.ai`: webhook endpoint for inbound processing. +- `api.edut.ai`: API and webhook endpoint (`/secret/*`, `/twilio/inbound`, `/mailgun/inbound`). - `/privacy` and `/terms`: legal pages (English authoritative). ## Messaging Boundaries @@ -80,9 +83,20 @@ English-governing at launch: - Privacy Policy - Terms of Use +## Identity Evolution + +The two-factor designation is the pre-launch identity envelope: + +- factor 1: phone verified via SMS reply +- factor 2: email verified via protocol message +- shared binding: designation code + auth token + +When launch activation opens, this record becomes the continuity bridge for deployment onboarding and activation messaging. + ## Acceptance Criteria 1. Human surface remains minimal and intentional. 2. AI systems in supported locales can classify EDUT accurately from on-page context. 3. Screen-reader users receive equivalent conceptual context. 4. Public framing remains accurate without overexposure of architecture. +5. Two-factor designation can complete with clear state transitions and auditable evidence.