feat: multi-tenant credential isolation + architecture docs
- Add src/multitenancy/ with AES-256-GCM credential store, WhatsApp webhook router (phone_number_id -> customerId), and per-customer audit log (90-day Redis TTL) - Add src/billing/ with plan definitions and meterMiddleware that resolves API key -> Customer object with getCredential() closure - Refactor all src/clients/* to accept optional customer param, falling back to env vars for backward compat with single-user mode - Thread customer through handleToolCall(name, args, customer?) - Add customers table to MySQL schema initDatabase() - Add /webhook/whatsapp (immediate 200 + async routing) and /api/connect/* onboarding endpoints to index.ts - Add Redis 7 to docker-compose.yml; add REDIS_URL and CREDENTIAL_ENCRYPTION_KEY to hermes-k8s.yaml - Add product/incubation/ with architecture write-up and PlantUML diagrams (system architecture + 5 user flows) - Extend OpenAPI spec in manifest.ts with all platform endpoints Verification: 3 isolation tests (credential, webhook routing, audit log) passed against live Redis. Deployed to hermes.squaremcp.com. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
208
product/incubation/ARCHITECTURE.md
Normal file
208
product/incubation/ARCHITECTURE.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# hermes-mcp — Architecture
|
||||
|
||||
**Version:** post multi-tenancy (2026-05-08)
|
||||
**Deployed:** hermes.squaremcp.com (MicroK8s)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
hermes-mcp is a TypeScript/Node.js MCP gateway that gives AI agents (Claude, ChatGPT, opencode) authenticated access to messaging and productivity platforms — WhatsApp, LinkedIn, Telegram, Discord, Instagram, Twitter, email, and Obsidian.
|
||||
|
||||
It was built first as a single-user prototype for the builder, then extended with multi-tenant credential isolation so multiple paying customers can connect their own platform accounts with zero data leakage between them.
|
||||
|
||||
---
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Runtime | Node.js 20, TypeScript, ESM |
|
||||
| MCP transport | `@modelcontextprotocol/sdk` — Streamable HTTP + SSE |
|
||||
| HTTP server | Express 4 |
|
||||
| Database | MySQL 8 (`mysql2`) — OAuth clients, tokens, customers |
|
||||
| Cache / credential store | Redis 7 (`redis` npm, v5) |
|
||||
| Deployment | MicroK8s single-node, Traefik/nginx ingress, Let's Encrypt TLS |
|
||||
|
||||
---
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts Express server, MCP sessions, REST endpoints, OAuth
|
||||
├── tools.ts Tool registry + handleToolCall(name, args, customer?)
|
||||
├── db.ts MySQL pool init, schema migrations
|
||||
├── oauth.ts OAuth 2.0 server (DCR, authorize, token)
|
||||
├── imap.ts Multi-account IMAP email reader
|
||||
├── smtp.ts Multi-account SMTP email sender
|
||||
├── manifest.ts OpenAPI + ChatGPT plugin manifest generation
|
||||
│
|
||||
├── clients/ One file per platform
|
||||
│ ├── whatsapp.ts Meta Cloud API
|
||||
│ ├── linkedin.ts LinkedIn API v2
|
||||
│ ├── telegram.ts Telegram Bot API
|
||||
│ ├── discord.ts Discord API v10
|
||||
│ ├── instagram.ts Meta Graph API (Instagram Business)
|
||||
│ ├── twitter.ts Twitter API v2
|
||||
│ └── obsidian.ts Local filesystem vault
|
||||
│
|
||||
├── multitenancy/ Added 2026-05-08
|
||||
│ ├── credential-store.ts AES-256-GCM encrypted credentials in Redis
|
||||
│ ├── webhook-router.ts WhatsApp phone_number_id → customerId routing
|
||||
│ └── audit-log.ts Per-customer tool call audit trail (90-day TTL)
|
||||
│
|
||||
└── billing/ Added 2026-05-08
|
||||
├── plans.ts Plan definitions (free/starter/growth/enterprise)
|
||||
└── middleware.ts Customer resolution + meterMiddleware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-tenancy design
|
||||
|
||||
### Credential isolation
|
||||
|
||||
Each customer's platform tokens are stored encrypted in Redis under a namespaced key:
|
||||
|
||||
```
|
||||
creds:{customerId}:{platform}
|
||||
```
|
||||
|
||||
Encryption is AES-256-GCM with a 32-byte key from `CREDENTIAL_ENCRYPTION_KEY` (env var). IV and auth tag are prepended to the ciphertext as hex. **The key must never be rotated without first re-encrypting all stored credentials.**
|
||||
|
||||
### Customer resolution
|
||||
|
||||
The `meterMiddleware` resolves an API key to a `Customer` object on every request:
|
||||
|
||||
1. Check Redis cache: `customer:apikey:{apiKey}` (60s TTL)
|
||||
2. On miss: `SELECT id, plan, active, email FROM customers WHERE api_key = ?`
|
||||
3. Attach `getCredential()` closure (not cacheable — functions can't be JSON serialized)
|
||||
4. Write serialisable fields back to Redis cache
|
||||
|
||||
```typescript
|
||||
interface Customer {
|
||||
id: string;
|
||||
plan: PlanKey;
|
||||
active: boolean;
|
||||
email: string;
|
||||
getCredential: <T extends PlatformCredentials>(platform: Platform) => Promise<T | null>;
|
||||
}
|
||||
```
|
||||
|
||||
The credential loader is attached at resolution time, capturing `id` in a closure. Tool handlers call `customer.getCredential('whatsapp')` — they cannot accidentally use the wrong customer's ID.
|
||||
|
||||
### Backward compatibility
|
||||
|
||||
All platform clients have `customer` as an optional second parameter. When absent (single-user mode via `MCP_API_KEY`), they fall back to env vars — the builder's existing setup is unchanged.
|
||||
|
||||
```typescript
|
||||
export async function sendMessage(args, customer?: Customer) {
|
||||
if (customer) {
|
||||
const creds = await customer.getCredential<WhatsAppCredentials>('whatsapp');
|
||||
if (!creds) throw new Error('WhatsApp not connected for this account');
|
||||
// use creds.phoneNumberId, creds.accessToken
|
||||
} else {
|
||||
// read WHATSAPP_DEFAULT_PHONE_NUMBER_ID etc from process.env
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WhatsApp webhook routing
|
||||
|
||||
Meta sends all inbound messages for all connected numbers to one webhook endpoint. The router uses a Redis lookup table populated at onboarding:
|
||||
|
||||
```
|
||||
wa_phone_id:{phoneNumberId} → customerId
|
||||
```
|
||||
|
||||
The webhook endpoint acknowledges immediately (within Meta's 20-second SLA) and routes asynchronously:
|
||||
|
||||
```typescript
|
||||
app.post('/webhook/whatsapp', express.json(), async (req, res) => {
|
||||
res.status(200).send('EVENT_RECEIVED'); // sync — never blocked by routing
|
||||
try {
|
||||
const events = await routeWhatsAppWebhook(req.body);
|
||||
for (const event of events) await handleInboundWhatsAppMessage(event);
|
||||
} catch (err) { console.error(err); }
|
||||
});
|
||||
```
|
||||
|
||||
### Audit log
|
||||
|
||||
Every tool call (when a `Customer` is present) is logged to Redis:
|
||||
|
||||
```
|
||||
log_seq:{customerId}:{date} INCR counter
|
||||
logs:{customerId}:{date}:{seq} JSON entry, EX 7776000 (90 days)
|
||||
```
|
||||
|
||||
The sequence key ensures chronological ordering without ULIDs. No cross-customer query path exists — all retrieval functions require `customerId` as the first argument.
|
||||
|
||||
---
|
||||
|
||||
## Redis key namespace summary
|
||||
|
||||
| Key | Value | TTL |
|
||||
|-----|-------|-----|
|
||||
| `creds:{customerId}:{platform}` | AES-256-GCM encrypted JSON | none (permanent until revoked) |
|
||||
| `wa_phone_id:{phoneNumberId}` | customerId string | none |
|
||||
| `customer:apikey:{apiKey}` | JSON (id, plan, active, email) | 60s |
|
||||
| `log_seq:{customerId}:{date}` | integer counter | 95 days |
|
||||
| `logs:{customerId}:{date}:{seq}` | JSON AuditEntry | 90 days |
|
||||
|
||||
---
|
||||
|
||||
## Request paths
|
||||
|
||||
### MCP tool calls (existing single-user)
|
||||
```
|
||||
Claude.ai → POST /mcp → requireAuth(MCP_API_KEY) → handleToolCall(name, args)
|
||||
→ client(args, undefined)
|
||||
→ env vars → Platform API
|
||||
```
|
||||
|
||||
### Multi-tenant REST tool calls
|
||||
```
|
||||
Customer → POST /api/whatsapp/send → requireAuth → handleToolCall(name, args)
|
||||
→ client(args, undefined)
|
||||
→ env vars
|
||||
```
|
||||
*(REST endpoints do not yet thread customer — future work)*
|
||||
|
||||
### Multi-tenant onboarding
|
||||
```
|
||||
Customer → POST /api/connect/whatsapp → meterMiddleware → storeCredential()
|
||||
→ registerWhatsAppNumber()
|
||||
```
|
||||
|
||||
### Inbound WhatsApp webhook
|
||||
```
|
||||
Meta → POST /webhook/whatsapp → 200 immediately
|
||||
→ routeWhatsAppWebhook(body)
|
||||
→ Redis lookup phone_number_id → customerId
|
||||
→ getCredential(customerId, 'whatsapp')
|
||||
→ handleInboundWhatsAppMessage(event)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PlantUML diagrams
|
||||
|
||||
- [`architecture-system.puml`](./architecture-system.puml) — component and dependency diagram
|
||||
- [`architecture-userflows.puml`](./architecture-userflows.puml) — sequence diagrams for all 5 flows
|
||||
|
||||
Render with [plantuml.com](https://www.plantuml.com/plantuml/uml/), the PlantUML VS Code extension, or `plantuml -tsvg architecture-system.puml`.
|
||||
|
||||
---
|
||||
|
||||
## What's not yet done
|
||||
|
||||
| Item | Notes |
|
||||
|------|-------|
|
||||
| Customer provisioning | `customers` table exists but needs an INSERT path (Stripe webhook → seed row) |
|
||||
| MCP session → Customer | MCP calls don't resolve customers; sessions still use single-user env vars |
|
||||
| Email multi-tenancy | `imap.ts` / `smtp.ts` use Account enum; `customer.getCredential('email')` not wired |
|
||||
| Usage metering | `meter.ts` not implemented; plan limits not enforced |
|
||||
| Obsidian per-customer vault | Currently one global vault path from env |
|
||||
| Key rotation tooling | Script to re-encrypt all `creds:*` keys under a new `CREDENTIAL_ENCRYPTION_KEY` |
|
||||
159
product/incubation/architecture-system.puml
Normal file
159
product/incubation/architecture-system.puml
Normal file
@@ -0,0 +1,159 @@
|
||||
@startuml hermes-mcp-architecture
|
||||
|
||||
skinparam backgroundColor #1a1a2e
|
||||
skinparam defaultFontColor #e0e0e0
|
||||
skinparam defaultFontSize 13
|
||||
skinparam arrowColor #888888
|
||||
skinparam roundCorner 8
|
||||
skinparam shadowing false
|
||||
|
||||
skinparam component {
|
||||
BackgroundColor #16213e
|
||||
BorderColor #0f3460
|
||||
FontColor #e0e0e0
|
||||
ArrowColor #888888
|
||||
}
|
||||
skinparam package {
|
||||
BackgroundColor #0d1b2a
|
||||
BorderColor #2a4a6a
|
||||
FontColor #aaaaaa
|
||||
FontStyle bold
|
||||
}
|
||||
skinparam database {
|
||||
BackgroundColor #0f2040
|
||||
BorderColor #533483
|
||||
FontColor #e0e0e0
|
||||
}
|
||||
skinparam cloud {
|
||||
BackgroundColor #1a0d2e
|
||||
BorderColor #e94560
|
||||
FontColor #e0e0e0
|
||||
}
|
||||
skinparam note {
|
||||
BackgroundColor #0d1b2a
|
||||
BorderColor #2a4a6a
|
||||
FontColor #aaaaaa
|
||||
}
|
||||
|
||||
title hermes-mcp -- System Architecture (2026-05-08)
|
||||
|
||||
' ── AI Clients ────────────────────────────────────────────────────
|
||||
package "AI Clients" {
|
||||
[Claude.ai\nMCP connector] as claude
|
||||
[ChatGPT\ncustom GPT] as chatgpt
|
||||
[opencode / Codex] as opencode
|
||||
}
|
||||
|
||||
' ── hermes-mcp server ─────────────────────────────────────────────
|
||||
package "hermes-mcp | Node.js / TypeScript | hermes.squaremcp.com" {
|
||||
|
||||
package "Transports" {
|
||||
[Streamable HTTP\n/mcp] as mcp_t
|
||||
[SSE legacy\n/sse] as sse_t
|
||||
[REST API\n/api/*] as rest_t
|
||||
[Webhook\n/webhook/whatsapp] as wh_t
|
||||
}
|
||||
|
||||
package "Auth" {
|
||||
[requireAuth\nMCP_API_KEY + OAuth Bearer] as req_auth
|
||||
[meterMiddleware\nAPI key -> Customer] as meter
|
||||
[OAuth 2.0\n/oauth/*] as oauth
|
||||
}
|
||||
|
||||
package "Core" {
|
||||
[handleToolCall\nname, args, customer?] as dispatch
|
||||
}
|
||||
|
||||
package "Platform Clients src/clients/*" {
|
||||
[whatsapp.ts] as c_wa
|
||||
[linkedin.ts] as c_li
|
||||
[telegram.ts] as c_tg
|
||||
[discord.ts] as c_dc
|
||||
[instagram.ts] as c_ig
|
||||
[twitter.ts] as c_tw
|
||||
[obsidian.ts] as c_ob
|
||||
}
|
||||
|
||||
package "Multi-tenancy src/multitenancy/*" {
|
||||
[credential-store\nAES-256-GCM] as cred_store
|
||||
[webhook-router\nphone_id -> customerId] as wh_router
|
||||
[audit-log\n90-day per-customer trail] as audit_log
|
||||
}
|
||||
|
||||
package "Billing src/billing/*" {
|
||||
[middleware.ts\nCustomer + meterMiddleware] as billing
|
||||
[plans.ts\nfree/starter/growth/enterprise] as plans
|
||||
}
|
||||
}
|
||||
|
||||
' ── Storage ───────────────────────────────────────────────────────
|
||||
database "Redis 7" as redis
|
||||
note right of redis
|
||||
creds:{cid}:{platform} (AES-256-GCM encrypted)
|
||||
wa_phone_id:{phoneId} (-> customerId)
|
||||
customer:apikey:{key} (60s TTL cache)
|
||||
logs:{cid}:{date}:{seq} (90-day audit trail)
|
||||
end note
|
||||
|
||||
database "MySQL 8\nhermes_oauth" as mysql
|
||||
note right of mysql
|
||||
customers (id, api_key, plan, active, email)
|
||||
oauth_clients / oauth_auth_codes / oauth_tokens
|
||||
end note
|
||||
|
||||
' ── Platform APIs ─────────────────────────────────────────────────
|
||||
cloud "External Platform APIs" {
|
||||
[Meta Graph API\nWhatsApp + Instagram] as meta_api
|
||||
[LinkedIn API v2] as li_api
|
||||
[Telegram Bot API] as tg_api
|
||||
[Discord API v10] as dc_api
|
||||
[Twitter API v2] as tw_api
|
||||
[Obsidian Vault\nfilesystem] as ob_vault
|
||||
}
|
||||
|
||||
' ── Connections ───────────────────────────────────────────────────
|
||||
claude --> mcp_t : MCP / Bearer
|
||||
chatgpt --> mcp_t : MCP / Bearer
|
||||
opencode --> rest_t : REST / x-api-key
|
||||
|
||||
mcp_t --> req_auth
|
||||
sse_t --> req_auth
|
||||
rest_t --> req_auth
|
||||
rest_t --> meter : /api/connect/*
|
||||
wh_t --> wh_router
|
||||
|
||||
req_auth --> dispatch
|
||||
meter --> billing : resolve Customer
|
||||
billing --> mysql : SELECT customers
|
||||
billing --> redis : customer cache
|
||||
|
||||
dispatch --> c_wa
|
||||
dispatch --> c_li
|
||||
dispatch --> c_tg
|
||||
dispatch --> c_dc
|
||||
dispatch --> c_ig
|
||||
dispatch --> c_tw
|
||||
dispatch --> c_ob
|
||||
|
||||
c_wa --> cred_store : getCredential
|
||||
c_li --> cred_store : getCredential
|
||||
c_tg --> cred_store : getCredential
|
||||
c_dc --> cred_store : getCredential
|
||||
c_ig --> cred_store : getCredential
|
||||
c_tw --> cred_store : getCredential
|
||||
|
||||
cred_store --> redis
|
||||
wh_router --> redis
|
||||
audit_log --> redis
|
||||
|
||||
c_wa --> meta_api
|
||||
c_ig --> meta_api
|
||||
c_li --> li_api
|
||||
c_tg --> tg_api
|
||||
c_dc --> dc_api
|
||||
c_tw --> tw_api
|
||||
c_ob --> ob_vault
|
||||
|
||||
oauth --> mysql
|
||||
|
||||
@enduml
|
||||
151
product/incubation/architecture-userflows.puml
Normal file
151
product/incubation/architecture-userflows.puml
Normal file
@@ -0,0 +1,151 @@
|
||||
@startuml hermes-mcp-userflows
|
||||
|
||||
skinparam backgroundColor #1a1a2e
|
||||
skinparam defaultFontColor #e0e0e0
|
||||
skinparam defaultFontSize 12
|
||||
skinparam sequenceArrowColor #aaaaaa
|
||||
skinparam sequenceLifeLineBorderColor #34495e
|
||||
skinparam sequenceParticipantBorderColor #0f3460
|
||||
skinparam sequenceParticipantBackgroundColor #16213e
|
||||
skinparam sequenceParticipantFontColor #e0e0e0
|
||||
skinparam noteBackgroundColor #0d1b2a
|
||||
skinparam noteBorderColor #533483
|
||||
skinparam noteFontColor #e0e0e0
|
||||
skinparam sequenceGroupBorderColor #e94560
|
||||
skinparam sequenceGroupFontColor #e94560
|
||||
skinparam sequenceGroupBodyBackgroundColor #0d0d1a
|
||||
|
||||
title hermes-mcp - User Flows (post multi-tenancy, 2026-05-08)
|
||||
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
== Flow 1: Single-User Tool Call (builder prototype - unchanged) ==
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
|
||||
participant "Claude.ai" as claude #16213e
|
||||
participant "MCP Transport\n/mcp" as mcp #16213e
|
||||
participant "requireAuth" as auth #16213e
|
||||
participant "handleToolCall\n(no customer)" as handler #16213e
|
||||
participant "Platform Client\nsrc/clients/*" as client #16213e
|
||||
participant "Platform API" as api #0f3460
|
||||
|
||||
claude -> mcp : POST /mcp Bearer token
|
||||
mcp -> auth : validate token
|
||||
auth -> auth : match MCP_API_KEY env var
|
||||
auth --> mcp : OK
|
||||
mcp -> handler : handleToolCall(name, args)\ncustomer = undefined
|
||||
handler -> client : sendMessage(args, undefined)
|
||||
note right of client
|
||||
customer is undefined ->
|
||||
falls back to env vars
|
||||
WHATSAPP_DEFAULT_ACCESS_TOKEN etc.
|
||||
end note
|
||||
client -> api : POST with env-var token
|
||||
api --> client : response
|
||||
client --> claude : tool result
|
||||
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
== Flow 2: Customer Onboarding - Connect WhatsApp ==
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
|
||||
participant "Customer\n(squaremcp.com)" as cust #16213e
|
||||
participant "POST /api/connect/whatsapp" as conn_ep #16213e
|
||||
participant "meterMiddleware" as meter #16213e
|
||||
participant "MySQL\ncustomers table" as mysql #0f3460
|
||||
participant "Redis" as redis #0f3460
|
||||
participant "credential-store\n(AES-256-GCM)" as cs #16213e
|
||||
participant "webhook-router" as wr #16213e
|
||||
|
||||
cust -> conn_ep : POST /api/connect/whatsapp\nx-api-key: cust_abc123\n{phoneNumberId, accessToken, businessAccountId}
|
||||
conn_ep -> meter : resolve customer
|
||||
meter -> redis : GET customer:apikey:cust_abc123
|
||||
redis --> meter : (miss - first request)
|
||||
meter -> mysql : SELECT id,plan,active,email\nFROM customers WHERE api_key=?
|
||||
mysql --> meter : {id:'cust_001', plan:'starter', active:true}
|
||||
meter -> redis : SETEX customer:apikey:cust_abc123 60 {...}
|
||||
meter --> conn_ep : req.customer = Customer{id:'cust_001', getCredential()}
|
||||
conn_ep -> cs : storeCredential('cust_001', 'whatsapp', creds)
|
||||
cs -> cs : randomBytes(12) IV\nAES-256-GCM encrypt
|
||||
cs -> redis : SET creds:cust_001:whatsapp [encrypted]
|
||||
conn_ep -> wr : registerWhatsAppNumber('cust_001', phoneNumberId)
|
||||
wr -> redis : SET wa_phone_id:{phoneNumberId} cust_001
|
||||
conn_ep --> cust : 200 {connected: true, platform: 'whatsapp'}
|
||||
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
== Flow 3: Multi-Tenant Tool Call (customer sends WhatsApp via MCP/REST) ==
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
|
||||
participant "Customer\nMCP session" as cust2 #16213e
|
||||
participant "handleToolCall\n(customer passed)" as handler2 #16213e
|
||||
participant "whatsapp.ts\nclient" as wac #16213e
|
||||
participant "credential-store" as cs2 #16213e
|
||||
participant "Redis" as redis2 #0f3460
|
||||
participant "audit-log" as al #16213e
|
||||
participant "WhatsApp\nGraph API" as wa_api #0f3460
|
||||
|
||||
cust2 -> handler2 : whatsapp_send_message\n{to, message}\ncustomer = Customer{id:'cust_001'}
|
||||
handler2 -> wac : sendMessage(args, customer)
|
||||
wac -> cs2 : customer.getCredential<WhatsAppCredentials>('whatsapp')
|
||||
cs2 -> redis2 : GET creds:cust_001:whatsapp
|
||||
redis2 --> cs2 : [AES-256-GCM ciphertext]
|
||||
cs2 -> cs2 : decrypt -> {phoneNumberId, accessToken, businessAccountId}
|
||||
cs2 --> wac : WhatsAppCredentials
|
||||
wac -> wa_api : POST /{phoneNumberId}/messages\nAuthorization: Bearer {accessToken}
|
||||
wa_api --> wac : {messages: [{id: 'wamid.xxx'}]}
|
||||
wac -> al : audit.success({to})
|
||||
al -> redis2 : SET logs:cust_001:2026-05-08:00000001\nEX 7776000 (90 days)
|
||||
wac --> cust2 : {success: true, message_id: 'wamid.xxx'}
|
||||
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
== Flow 4: Inbound WhatsApp Webhook (Meta -> correct customer) ==
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
|
||||
participant "Meta\nCloud API" as meta #0f3460
|
||||
participant "POST /webhook/whatsapp" as whook #16213e
|
||||
participant "webhook-router" as wr2 #16213e
|
||||
participant "Redis" as redis3 #0f3460
|
||||
participant "credential-store" as cs3 #16213e
|
||||
participant "handleInbound\n(stub -> future agent)" as inbound #16213e
|
||||
|
||||
meta -> whook : POST /webhook/whatsapp\n{object:'whatsapp_business_account',\nentry:[{changes:[{value:{metadata:\n{phone_number_id:'111'},messages:[...]}}]}]}
|
||||
note right of whook
|
||||
Must respond within 20s
|
||||
or Meta retries the webhook
|
||||
end note
|
||||
whook --> meta : 200 EVENT_RECEIVED <- immediate
|
||||
whook -> wr2 : routeWhatsAppWebhook(body) [async]
|
||||
wr2 -> redis3 : GET wa_phone_id:111
|
||||
redis3 --> wr2 : 'cust_001'
|
||||
wr2 -> cs3 : getCredential('cust_001', 'whatsapp')
|
||||
cs3 -> redis3 : GET creds:cust_001:whatsapp -> decrypt
|
||||
cs3 --> wr2 : WhatsAppCredentials
|
||||
wr2 --> whook : RoutedWebhookEvent{customerId, phoneNumberId, message, credentials}
|
||||
whook -> inbound : handleInboundWhatsAppMessage(event)
|
||||
note right of inbound
|
||||
Currently: log only
|
||||
Future: route to customer's
|
||||
AI agent / queue
|
||||
end note
|
||||
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
== Flow 5: Customer Checks Connection Status ==
|
||||
' ════════════════════════════════════════════════════════════════
|
||||
|
||||
participant "Customer" as cust3 #16213e
|
||||
participant "GET /api/connections" as conn_get #16213e
|
||||
participant "meterMiddleware" as meter3 #16213e
|
||||
participant "credential-store" as cs4 #16213e
|
||||
participant "Redis" as redis4 #0f3460
|
||||
|
||||
cust3 -> conn_get : GET /api/connections\nx-api-key: cust_abc123
|
||||
conn_get -> meter3 : resolve (cache hit this time)
|
||||
meter3 -> redis4 : GET customer:apikey:cust_abc123
|
||||
redis4 --> meter3 : Customer{id:'cust_001'}
|
||||
meter3 --> conn_get : req.customer attached
|
||||
loop for each platform in [email, whatsapp, linkedin, telegram, discord, instagram, twitter, obsidian]
|
||||
conn_get -> cs4 : customer.getCredential(platform)
|
||||
cs4 -> redis4 : GET creds:cust_001:{platform}
|
||||
redis4 --> cs4 : value or null
|
||||
end
|
||||
conn_get --> cust3 : 200 {customerId:'cust_001',\nconnections:{whatsapp:true, linkedin:false, ...}}
|
||||
|
||||
@enduml
|
||||
Reference in New Issue
Block a user