feat: Client-published events + realtime presence

- POST /projects/:id/events accepts client-authored events restricted to
  the custom.* namespace — system types can't be spoofed. They flow
  through the same publishEvent fan-out: realtime, webhooks, and
  event_pattern functions all fire.
- Presence: subscribe/unsubscribe bumps a per-project count and
  broadcasts presence.update {online} to the realtime stream only —
  deliberately not through publishEvent so webhooks and "*" functions
  don't fire on connect noise. GET …/realtime/presence reads the count.
- @primora/client gains publish() and presence(); CLI gains events:send.

Verified live: presence 0->1 on subscribe, presence.update frame
delivered, custom event delivered to SSE + fired a custom.* function,
spoofed document.created rejected 400.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 20:55:48 +02:00
1 parent 9ae1c22af0
commit 236357facb
16 files changed
+393 -10

No files matched your search

+65
View File
@@ -1331,6 +1331,37 @@ paths:
$ref: "#/components/schemas/TelemetryEventListResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
post:
tags: [Automation]
operationId: publishProjectEvent
summary: Publishes a client-authored domain event. Type must be in the custom.* namespace; the event fans out to realtime subscribers, matching webhooks, and event_pattern functions.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PublishEventRequest"
responses:
"202":
description: Event accepted for fan-out
content:
application/json:
schema:
type: object
properties:
published:
type: string
"400":
$ref: "#/components/responses/ErrorResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
"403":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/issues:
get:
tags: [Telemetry]
@@ -1887,6 +1918,28 @@ paths:
description: text/event-stream of {type, occurred_at, data} messages
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/realtime/presence:
get:
tags: [Automation]
operationId: getRealtimePresence
summary: Count of clients currently subscribed to the project's realtime stream. The stream also emits presence.update events on every join/leave.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
responses:
"200":
description: Online subscriber count
content:
application/json:
schema:
type: object
properties:
online:
type: integer
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/secrets:
get:
tags: [Secrets]
@@ -3191,6 +3244,18 @@ components:
type: string
format: date-time
required: [url, method, expires_at]
PublishEventRequest:
type: object
properties:
type:
type: string
pattern: '^custom\.[a-z0-9][a-z0-9_.-]{0,62}$'
description: Event type in the custom.* namespace — realtime subscribers, matching webhooks, and event_pattern functions all fire.
data:
type: object
additionalProperties: true
description: Arbitrary JSON payload carried to subscribers.
required: [type]
PaginationMetadata:
type: object
properties: