Files
hermes-mcp/product/incubation/architecture-userflows.puml
Garfield 8d62e4d9d5 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>
2026-05-08 11:27:29 -04:00

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