- 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>
152 lines
7.7 KiB
Plaintext
152 lines
7.7 KiB
Plaintext
@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
|