Add two-factor designation flow with Twilio + Mailgun and update vision
This commit is contained in:
parent
2fa83e1ed8
commit
d015135073
@ -1,158 +1,182 @@
|
|||||||
# Edut Secret System — Deployment Spec
|
# Edut Secret System - Two-Factor Designation Spec
|
||||||
|
|
||||||
## Overview
|
## 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
|
Visitor clicks orb on edut.ai
|
||||||
↓
|
-> phone input appears
|
||||||
Email client opens: mailto:0217073045482@secret.edut.ai
|
-> user submits phone
|
||||||
↓
|
POST /secret/initiate
|
||||||
Person sends email
|
-> code issued
|
||||||
↓
|
-> row created/updated in SQLite
|
||||||
Mailgun receives on catch-all (*@secret.edut.ai)
|
-> SMS sent via Twilio
|
||||||
↓
|
User receives SMS and replies: CONFIRM
|
||||||
Mailgun POSTs to https://api.edut.ai/mailgun/inbound
|
Twilio POST /twilio/inbound
|
||||||
↓
|
-> signature verified
|
||||||
Webhook parses email, stores in SQLite, sends classified reply
|
-> pending designation matched
|
||||||
↓
|
-> phone_verified_at set
|
||||||
Reply threads under original email in sender's inbox
|
Landing page polls /secret/status/{code}
|
||||||
↓
|
-> sees phone_verified=true
|
||||||
Copy of inbound + reply also forwarded to j@edut.ai (Gmail archive)
|
-> 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 |
|
## Twilio Channel Design
|
||||||
|---------|--------|---------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Mailgun Configuration
|
### Number strategy
|
||||||
- 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
|
|
||||||
|
|
||||||
---
|
- 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
|
Designation: {token}
|
||||||
server {
|
Reply CONFIRM to proceed.
|
||||||
listen 80;
|
━━━━━━━━━━━━━━━━━
|
||||||
server_name api.edut.ai;
|
```
|
||||||
|
|
||||||
location /mailgun/inbound {
|
Where `{token}` is formatted from the code:
|
||||||
proxy_pass http://127.0.0.1:3847;
|
|
||||||
proxy_set_header Host $host;
|
- code: `0217073045482`
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
- token: `0217-0730-4548-2`
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
## 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:
|
||||||
|
|
||||||
---
|
```json
|
||||||
|
{
|
||||||
## Component 2: Webhook Service
|
"code": "0217073045482",
|
||||||
|
"token": "0217-0730-4548-2",
|
||||||
### Language
|
"status": "sms_sent"
|
||||||
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 <protocol@secret.edut.ai>"
|
|
||||||
- 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
## 2) Status
|
||||||
- `token` — Random string
|
|
||||||
- `signature` — HMAC-SHA256 of `timestamp + token` using your Mailgun webhook signing key
|
|
||||||
|
|
||||||
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.
|
```json
|
||||||
- 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.
|
"code": "0217073045482",
|
||||||
- If Mailgun send fails, retry with bounded backoff; if still failing, return `5xx`.
|
"phone_verified": true,
|
||||||
- Return `2xx` only after persistence succeeds and outbound reply is accepted or deterministically deduped.
|
"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
|
Classification: OBSERVER
|
||||||
Status: ACKNOWLEDGED
|
Status: ACKNOWLEDGED
|
||||||
|
|
||||||
|
Phone: VERIFIED
|
||||||
|
Email: VERIFIED
|
||||||
Timestamp: {iso_timestamp}
|
Timestamp: {iso_timestamp}
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@ -183,182 +209,124 @@ deterministic governance systems.
|
|||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
```
|
```
|
||||||
|
|
||||||
### Variable Substitutions
|
## SQLite Schema
|
||||||
|
|
||||||
| 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 <protocol@secret.edut.ai>
|
|
||||||
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
|
|
||||||
|
|
||||||
### File: `/var/lib/edut/secrets.db`
|
### File: `/var/lib/edut/secrets.db`
|
||||||
|
|
||||||
### Schema:
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE IF NOT EXISTS designations (
|
CREATE TABLE IF NOT EXISTS designations (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
code TEXT NOT NULL UNIQUE,
|
code TEXT NOT NULL UNIQUE,
|
||||||
email TEXT NOT NULL,
|
phone TEXT,
|
||||||
recipient TEXT NOT NULL,
|
phone_verified_at DATETIME,
|
||||||
|
email TEXT,
|
||||||
|
email_verified_at DATETIME,
|
||||||
|
recipient TEXT,
|
||||||
message_id TEXT,
|
message_id TEXT,
|
||||||
auth_token TEXT NOT NULL,
|
auth_token TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending_phone',
|
||||||
created_at DATETIME DEFAULT (datetime('now')),
|
created_at DATETIME DEFAULT (datetime('now')),
|
||||||
replied_at DATETIME,
|
replied_at DATETIME,
|
||||||
reply_status TEXT DEFAULT 'pending'
|
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_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 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
|
## Environment Variables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
MAILGUN_API_KEY=<sending API key for secret.edut.ai>
|
|
||||||
MAILGUN_DOMAIN=secret.edut.ai
|
|
||||||
MAILGUN_SIGNING_KEY=<from Mailgun account settings, for webhook verification>
|
|
||||||
SECRETS_DB_PATH=/var/lib/edut/secrets.db
|
SECRETS_DB_PATH=/var/lib/edut/secrets.db
|
||||||
AUTH_SALT=<random 32+ character string for hashing auth tokens>
|
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
|
||||||
|
|
||||||
## 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
|
- Signature verification on both inbound providers.
|
||||||
[Unit]
|
- Per-IP and per-phone initiate throttles.
|
||||||
Description=Edut Secret Webhook
|
- Per-code polling throttles.
|
||||||
After=network.target
|
- Do not expose auth token except in confirmation email.
|
||||||
|
- Back up SQLite and protect file permissions.
|
||||||
|
|
||||||
[Service]
|
## Launch Evolution
|
||||||
Type=simple
|
|
||||||
User=www-data
|
|
||||||
WorkingDirectory=/opt/edut/secret
|
|
||||||
ExecStart=/opt/edut/secret/secret-server
|
|
||||||
EnvironmentFile=/opt/edut/secret/.env
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
At launch, each fully verified designation (`phone_verified_at` + `email_verified_at`) is eligible for activation messaging:
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
- 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`
|
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.
|
||||||
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 |
|
|
||||||
|
|||||||
@ -10,15 +10,18 @@
|
|||||||
|
|
||||||
The site should feel precise and established while avoiding disclosure of private implementation IP.
|
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`.
|
1. A visitor lands on `edut.ai`.
|
||||||
2. They see only the globe, identity line, and meaning line.
|
2. They see only the globe, identity line, and meaning line.
|
||||||
3. They click anywhere.
|
3. They click anywhere.
|
||||||
4. The globe accelerates and a pre-filled protocol-style email opens.
|
4. A minimal phone field appears inline (same aesthetic language).
|
||||||
5. The recipient is a timestamp address (`{timestamp}@secret.edut.ai`).
|
5. They enter their number and submit.
|
||||||
6. They send it and receive a designation confirmation with auth token.
|
6. They receive SMS from Edut protocol channel with designation token and `CONFIRM` instruction.
|
||||||
7. At launch, the same designation receives a follow-up changing classification from `OBSERVER` to `OPERATOR` with status `ACTIVE`.
|
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.
|
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.ai`: primary public surface and business identity.
|
||||||
- `edut.dev`: developer-facing domain (same landing for now).
|
- `edut.dev`: developer-facing domain (same landing for now).
|
||||||
- `secret.edut.ai`: inbound designation email namespace.
|
- `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).
|
- `/privacy` and `/terms`: legal pages (English authoritative).
|
||||||
|
|
||||||
## Messaging Boundaries
|
## Messaging Boundaries
|
||||||
@ -80,9 +83,20 @@ English-governing at launch:
|
|||||||
- Privacy Policy
|
- Privacy Policy
|
||||||
- Terms of Use
|
- 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
|
## Acceptance Criteria
|
||||||
|
|
||||||
1. Human surface remains minimal and intentional.
|
1. Human surface remains minimal and intentional.
|
||||||
2. AI systems in supported locales can classify EDUT accurately from on-page context.
|
2. AI systems in supported locales can classify EDUT accurately from on-page context.
|
||||||
3. Screen-reader users receive equivalent conceptual context.
|
3. Screen-reader users receive equivalent conceptual context.
|
||||||
4. Public framing remains accurate without overexposure of architecture.
|
4. Public framing remains accurate without overexposure of architecture.
|
||||||
|
5. Two-factor designation can complete with clear state transitions and auditable evidence.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user