mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-26 08:00:42 +00:00
231 lines
10 KiB
JSON
231 lines
10 KiB
JSON
{
|
|
"asyncapi": "3.1.0",
|
|
"info": {
|
|
"title": "Warmbly Realtime Gateway",
|
|
"version": "1.0.0",
|
|
"description": "Resumable WebSocket gateway for live organization events. The socket speaks the Phoenix channel protocol (serializer 1.0.0). Authenticate with an API key (REALTIME_SUBSCRIBE permission) or a short-lived JWT passed as the `token` query parameter. Events carry a monotonic per-organization `seq`; reconnecting clients resume by replaying the gap. See the human guide at https://docs.warmbly.com/api/realtime/.",
|
|
"contact": { "name": "Warmbly", "url": "https://docs.warmbly.com" },
|
|
"license": { "name": "Proprietary", "url": "https://warmbly.com" }
|
|
},
|
|
"defaultContentType": "application/json",
|
|
"servers": {
|
|
"production": {
|
|
"host": "realtime.warmbly.com",
|
|
"pathname": "/socket/websocket",
|
|
"protocol": "wss",
|
|
"description": "Production gateway. Connect with ?vsn=1.0.0&token=<TOKEN>.",
|
|
"security": [{ "$ref": "#/components/securitySchemes/token" }]
|
|
}
|
|
},
|
|
"channels": {
|
|
"org": {
|
|
"address": "org:{org_id}",
|
|
"title": "Organization channel",
|
|
"description": "Per-organization event stream + team presence. A subscriber receives every event the member/key is permitted to see, optionally narrowed by intents. Messages are Phoenix channel frames [join_ref, ref, topic, event, payload].",
|
|
"parameters": {
|
|
"org_id": { "description": "The organization id (UUID) to subscribe to." }
|
|
},
|
|
"messages": {
|
|
"join": { "$ref": "#/components/messages/Join" },
|
|
"hello": { "$ref": "#/components/messages/Hello" },
|
|
"event": { "$ref": "#/components/messages/Event" },
|
|
"resumed": { "$ref": "#/components/messages/Resumed" },
|
|
"resumeFailed": { "$ref": "#/components/messages/ResumeFailed" },
|
|
"rateLimited": { "$ref": "#/components/messages/RateLimited" },
|
|
"presenceState": { "$ref": "#/components/messages/PresenceState" },
|
|
"presenceDiff": { "$ref": "#/components/messages/PresenceDiff" },
|
|
"presenceUpdate": { "$ref": "#/components/messages/PresenceUpdate" }
|
|
}
|
|
}
|
|
},
|
|
"operations": {
|
|
"joinOrg": {
|
|
"action": "send",
|
|
"channel": { "$ref": "#/channels/org" },
|
|
"summary": "Join the org channel (optionally with intents and a resume token).",
|
|
"messages": [{ "$ref": "#/channels/org/messages/join" }],
|
|
"reply": {
|
|
"channel": { "$ref": "#/channels/org" },
|
|
"messages": [{ "$ref": "#/channels/org/messages/hello" }]
|
|
}
|
|
},
|
|
"receiveOrgStream": {
|
|
"action": "receive",
|
|
"channel": { "$ref": "#/channels/org" },
|
|
"summary": "Receive live events, resume markers, presence, and rate-limit notices.",
|
|
"messages": [
|
|
{ "$ref": "#/channels/org/messages/event" },
|
|
{ "$ref": "#/channels/org/messages/resumed" },
|
|
{ "$ref": "#/channels/org/messages/resumeFailed" },
|
|
{ "$ref": "#/channels/org/messages/rateLimited" },
|
|
{ "$ref": "#/channels/org/messages/presenceState" },
|
|
{ "$ref": "#/channels/org/messages/presenceDiff" }
|
|
]
|
|
},
|
|
"updatePresence": {
|
|
"action": "send",
|
|
"channel": { "$ref": "#/channels/org" },
|
|
"summary": "Update your own presence activity (JWT members only).",
|
|
"messages": [{ "$ref": "#/channels/org/messages/presenceUpdate" }]
|
|
}
|
|
},
|
|
"components": {
|
|
"securitySchemes": {
|
|
"token": {
|
|
"type": "httpApiKey",
|
|
"in": "query",
|
|
"name": "token",
|
|
"description": "An API key with the REALTIME_SUBSCRIBE permission, or a short-lived connection JWT, passed as the `token` query parameter on the socket URL."
|
|
}
|
|
},
|
|
"messages": {
|
|
"Join": {
|
|
"name": "phx_join",
|
|
"title": "Join (phx_join)",
|
|
"summary": "Subscribe to org:{org_id}. Optionally declare intents and a resume token.",
|
|
"payload": { "$ref": "#/components/schemas/JoinPayload" }
|
|
},
|
|
"Hello": {
|
|
"name": "phx_reply",
|
|
"title": "HELLO (join reply)",
|
|
"summary": "Heartbeat cadence and the current stream sequence.",
|
|
"payload": { "$ref": "#/components/schemas/Hello" }
|
|
},
|
|
"Event": {
|
|
"name": "event",
|
|
"title": "Domain event",
|
|
"summary": "A live or replayed event. The frame event name is the event_type (e.g. CAMPAIGN_UPDATED); the payload carries the body plus a monotonic seq.",
|
|
"payload": { "$ref": "#/components/schemas/Event" }
|
|
},
|
|
"Resumed": {
|
|
"name": "resumed",
|
|
"title": "Resume complete",
|
|
"summary": "Sent after a successful resume replay; the events preceded it.",
|
|
"payload": { "$ref": "#/components/schemas/Resumed" }
|
|
},
|
|
"ResumeFailed": {
|
|
"name": "resume_failed",
|
|
"title": "Resume failed",
|
|
"summary": "The buffer no longer covers your position (or the token was malformed). Do a full REST resync, then continue from current_seq.",
|
|
"payload": { "$ref": "#/components/schemas/ResumeFailed" }
|
|
},
|
|
"RateLimited": {
|
|
"name": "rate_limited",
|
|
"title": "Outbound rate limited",
|
|
"payload": { "$ref": "#/components/schemas/RateLimited" }
|
|
},
|
|
"PresenceState": {
|
|
"name": "presence_state",
|
|
"title": "Presence snapshot",
|
|
"payload": { "$ref": "#/components/schemas/PresenceState" }
|
|
},
|
|
"PresenceDiff": {
|
|
"name": "presence_diff",
|
|
"title": "Presence diff",
|
|
"payload": { "$ref": "#/components/schemas/PresenceDiff" }
|
|
},
|
|
"PresenceUpdate": {
|
|
"name": "presence:update",
|
|
"title": "Update own presence",
|
|
"payload": { "$ref": "#/components/schemas/PresenceUpdate" }
|
|
}
|
|
},
|
|
"schemas": {
|
|
"JoinPayload": {
|
|
"type": "object",
|
|
"description": "Payload of the phx_join frame for org:{org_id}.",
|
|
"properties": {
|
|
"intents": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"description": "Optional event-family tokens (case-insensitive substring of the event type, e.g. CAMPAIGN, EMAIL, AUDIT). Absent/empty = the full permitted stream. Not a security boundary."
|
|
},
|
|
"resume": {
|
|
"type": "object",
|
|
"description": "Resume token from a reconnecting client.",
|
|
"properties": {
|
|
"last_seq": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "The highest seq the client has processed. The server replays events with seq greater than this."
|
|
}
|
|
},
|
|
"required": ["last_seq"]
|
|
}
|
|
}
|
|
},
|
|
"Hello": {
|
|
"type": "object",
|
|
"required": ["org_id", "heartbeat_interval_ms", "server_timeout_ms", "seq"],
|
|
"properties": {
|
|
"org_id": { "type": "string", "format": "uuid" },
|
|
"role": { "type": "string", "description": "The member's role in the org (JWT sockets)." },
|
|
"heartbeat_interval_ms": { "type": "integer", "description": "Send a heartbeat on the phoenix topic at least this often." },
|
|
"server_timeout_ms": { "type": "integer", "description": "The server closes the socket after this long with no heartbeat." },
|
|
"seq": { "type": "integer", "description": "Current stream sequence. A fresh client should start tracking from here." },
|
|
"resume_supported": { "type": "boolean" }
|
|
}
|
|
},
|
|
"Event": {
|
|
"type": "object",
|
|
"required": ["event_type", "seq"],
|
|
"description": "Invalidation-oriented: the body carries ids, not full resource state. Refetch over REST for current contents. Permission- and intent-filtered identically for live and replayed events.",
|
|
"properties": {
|
|
"event_type": { "type": "string", "description": "e.g. AUDIT_CREATED, CAMPAIGN_UPDATED, EMAIL_SENT, EMAIL_RECEIVED. Also the frame event name." },
|
|
"seq": { "type": "integer", "description": "Monotonic per-organization sequence. Track the highest seen; dedupe replays by it." },
|
|
"org_id": { "type": "string", "format": "uuid" },
|
|
"user_id": { "type": "string", "format": "uuid" },
|
|
"campaign_id": { "type": "string", "format": "uuid" },
|
|
"email_account_id": { "type": "string", "format": "uuid" }
|
|
},
|
|
"additionalProperties": true
|
|
},
|
|
"Resumed": {
|
|
"type": "object",
|
|
"required": ["from", "current_seq", "replayed"],
|
|
"properties": {
|
|
"from": { "type": "integer", "description": "The last_seq the client resumed from." },
|
|
"current_seq": { "type": "integer", "description": "The stream sequence after replay; the client is now caught up to here." },
|
|
"replayed": { "type": "integer", "description": "Number of events delivered during replay (after permission + intent filtering)." }
|
|
}
|
|
},
|
|
"ResumeFailed": {
|
|
"type": "object",
|
|
"required": ["reason", "current_seq"],
|
|
"properties": {
|
|
"reason": { "type": "string", "enum": ["buffer_evicted", "invalid_resume"] },
|
|
"current_seq": { "type": "integer", "description": "Resync over REST, then continue live from here." }
|
|
}
|
|
},
|
|
"RateLimited": {
|
|
"type": "object",
|
|
"properties": {
|
|
"category": { "type": "string", "example": "ws_message" },
|
|
"retry_after_ms": { "type": "integer" }
|
|
}
|
|
},
|
|
"PresenceState": {
|
|
"type": "object",
|
|
"description": "Phoenix presence snapshot: a map of user_id -> { metas: [...] }.",
|
|
"additionalProperties": true
|
|
},
|
|
"PresenceDiff": {
|
|
"type": "object",
|
|
"description": "Phoenix presence diff with joins and leaves.",
|
|
"properties": {
|
|
"joins": { "type": "object", "additionalProperties": true },
|
|
"leaves": { "type": "object", "additionalProperties": true }
|
|
}
|
|
},
|
|
"PresenceUpdate": {
|
|
"type": "object",
|
|
"properties": {
|
|
"page": { "type": "string" },
|
|
"resource": { "type": "string", "description": "e.g. thread:<id>, campaign:<id>, contact:<id>." },
|
|
"action": { "type": "string", "enum": ["viewing", "editing", "replying", "idle"] }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|