bus-api-provider-events — Bus Events API provider

Events API

bus-api-provider-events is the HTTP controller for the public Bus Events API. It accepts authenticated event publishing requests and exposes authenticated event streams. Provider and integration workers use the event contracts without adding their own public HTTP event controllers.

Bus API providers and integrations use Events for request/reply workflows such as runtime wake-up, container runner work, billing status, usage export, and Stripe provider calls. End users may also use event APIs when the deployment grants the required resource access, but event access is still identity-bound and resource-limited.

Authentication

The provider verifies the normal Bus API JWT audience ai.hg.fi/api. Send the token as Authorization: Bearer <JWT> on publish and stream requests. For the local curl checks below, first start the provider on port 8081 with the same HS256 secret used to mint the token:

BUS_API_JWT_SECRET=not-a-secret-local-development-hs256-key \
bus-api-provider-events --addr 127.0.0.1:8081 --events-backend memory

Keep that provider running in one terminal. In another terminal, mint a token with the narrow resources needed by the event namespace:

mkdir -p ./local
BUS_AUTH_HS256_SECRET=not-a-secret-local-development-hs256-key \
bus operator token --format token issue --local \
  --subject events-local \
  --audience ai.hg.fi/api \
  --scope "events:send events:listen" \
  --ttl 1h > ./local/events.token

Production deployments should use the normal auth or service-token flow and grant resources such as llm:proxy, container:run, or billing:read instead of broad event resources whenever the event namespace is protected.

The owner identity is derived from JWT sub. Callers do not provide identity IDs for authorization.

Unprotected event names require events:send to publish and events:listen to stream.

The provider stamps the event owner as identity_id from the JWT. Caller-supplied identity metadata is not trusted for authorization. Streams only return events the token is allowed to receive, and user tokens cannot subscribe to unrelated identities.

Protected Bus integration events use identities resource access. VM events use vm:read or vm:write; LLM execution events use llm:proxy; public container events use container:read, container:run, and container:delete; protected container runner administration events use container:admin; usage events use usage:write, usage:read, and usage:delete; SSH run events use ssh:run. Billing events are protected the same way: public status/setup events use billing:read or billing:setup, entitlement checks use billing:entitlement:check, subscription updates use billing:subscription:write, billing usage export uses billing:usage:export, and Stripe-provider events use billing:provider. Deploy/bootstrap events use cloud:read, cloud:write, cloud:destroy, database:read, database:admin, node:read, node:admin, inference:read, and inference:admin according to the namespace action. Wildcard streams are rejected by default because the provider cannot safely prove that one token may receive every future protected event.

Canonical task events use the protected bus.task.* namespace with concrete resources such as task:send, task:read, task:reply, task:claim, and task:admin. Canonical worker lifecycle and control events use bus.workers.* with workers:read, workers:write, workers:control, and workers:admin. Legacy generic work streams under bus.work.* and legacy development task streams under bus.dev.task.* remain protected compatibility namespaces with their existing work:* and dev:task:* resources. Future deployments may further qualify these resources by owner or repository, for example task:claim:busdk/bus-ledger or workers:control:dev-hg.

The internal event backend is selectable. memory is non-durable and intended for local development. Redis is available through Redis Streams with atomic XADD operations. PostgreSQL is available as a disposable durable event log that creates its minimal tables at startup. These backends plug in behind the same event bus boundary, so provider workers and HTTP handlers keep the same public behavior when the backend changes.

POST /api/v1/events

Publishes one event.

The provider stamps the event owner identity from the JWT. Caller-supplied identity metadata is not trusted for ownership or stream authorization. Send Content-Type: application/json with an event envelope:

{
  "name": "example.ping",
  "correlationId": "optional-correlation-id",
  "payload": {"ok": true}
}

name is required. payload may be any JSON value. Unicast delivery is selected on stream URLs with delivery=unicast and group=<group>, not in the publish body. Success returns 202 Accepted or 200 OK with acceptance metadata such as accepted, id, and name. Bad JSON or invalid event names return 400, missing auth returns 401, and missing resource access returns 403.

Runnable local publish check:

curl -fsS -X POST \
  -H "Authorization: Bearer $(cat ./local/events.token)" \
  -H "Content-Type: application/json" \
  http://127.0.0.1:8081/api/v1/events \
  -d '{"name":"example.ping","correlationId":"docs-ping","payload":{"ok":true}}'

Success returns 202 Accepted or 200 OK with acceptance metadata including the event id and name.

GET /api/v1/events/stream?name=<event-name>&delivery=broadcast

Streams matching events to every authorized listener.

Use broadcast delivery when all subscribers should receive the same event. The response is application/x-ndjson: each line is one JSON event envelope. Clients should read incrementally until the connection closes or their timeout expires.

Use replay=true&follow=false for a deterministic one-shot local stream check after publishing example.ping:

curl -fsS \
  -H "Authorization: Bearer $(cat ./local/events.token)" \
  'http://127.0.0.1:8081/api/v1/events/stream?name=example.ping&delivery=broadcast&replay=true&follow=false'

The first NDJSON line should be a JSON event envelope whose name is example.ping and whose correlationId is docs-ping.

Missing or invalid bearer tokens return 401 invalid_auth. Missing listen scope for the requested event namespace returns 403 forbidden. Invalid event names return 400 bad_request, and wildcard names are rejected unless the deployment explicitly enables broad admin-only event scopes.

GET /api/v1/events/stream?name=<event-name>&delivery=unicast&group=<group>&consumer=<consumer>

Streams matching events to one consumer in a group.

Use unicast delivery when only one worker in a group should receive each event. If group is omitted, the provider uses default. If consumer is omitted, the provider uses default; long-running workers should set a stable consumer name for logs and backend diagnostics. Unicast streams use the same newline-delimited JSON framing as broadcast streams. Only one authorized consumer in the group receives each event. Use short stable URL-safe group and consumer names such as billing-worker or usage-collector-1. Avoid spaces, slashes, control characters, and secrets; backend-specific invalid names return 400 bad_request or a stream setup error. Invalid group or consumer values return 400 bad_request; missing or underscoped authorization returns 401 or 403 before any stream is opened.

name=<event-name>

Selects the event name to publish or stream.

Protected Bus event names require resource access such as vm:write, container:run, billing:read, or usage:read. Event names are dot-separated lowercase identifiers such as bus.vm.status.request or example.ping. Use letters, digits, hyphen, underscore, and dots; avoid wildcards unless the deployment explicitly enables admin-only broad listening. Protected namespace resource mapping is documented in the authentication section above and in Bus API JWT audiences and scopes.

delivery=broadcast

Delivers each event to every authorized listener.

delivery=unicast

Delivers each event to one authorized consumer in the selected group.

group=<group>

Selects the unicast delivery group.

Workers in the same group compete for events. Workers in different groups each receive their own group delivery.

consumer=<consumer>

Identifies one unicast delivery consumer.

Use stable consumer names for long-running workers.

replay=true

Includes existing matching events before following new events.

follow=false

Returns after the replayed snapshot instead of waiting for new events.

Development

For local development, use an obvious non-secret JWT secret and locally generated test tokens only. Plain secret values are raw text even when they look like base64; use base64:<value> only for an intentionally base64-encoded secret. Do not commit real deployment secrets.

The BusDK superproject compose.yaml starts this provider as bus-events with --events-backend postgres and BUS_EVENTS_POSTGRES_DSN pointing at the local PostgreSQL service. Other local AI Platform services reach it at http://bus-events:8081, and nginx exposes /api/v1/events on the local public API port. The shared local token resources include the resources needed for LLM, billing, VM, container, usage, Stripe, work, and development-task events, so the compose stack can exercise request/reply workflows without enabling broad wildcard event access.

Production Notes

Use Redis or PostgreSQL for deployments that need restart tolerance or multiple processes. Select Redis with --events-backend redis, BUS_EVENTS_REDIS_ADDR, optional BUS_EVENTS_REDIS_PASSWORD, and optional BUS_EVENTS_REDIS_PREFIX. Select PostgreSQL with --events-backend postgres and BUS_EVENTS_POSTGRES_DSN. Use memory only for local development. Keep wildcard streaming disabled unless an explicitly trusted internal/admin deployment needs it and the token audience/resource policy allows it.

The PostgreSQL backend is intentionally migration-free. It creates bus_events and bus_event_group_cursors when missing. Use PostgreSQL for production deployments that need restart tolerance, multiple API processes, replayable event history, or durable work-group cursors. Destroying the PostgreSQL database loses queued events, replay history, and unicast delivery cursors, so do that only for disposable local or test environments. PostgreSQL uses LISTEN/NOTIFY to wake listeners quickly, with SQL polling as the fallback and SQL transactions as the source of truth.

Provider and integration processes should use narrow tokens with only the resources needed for the events they send and receive. Do not log bearer tokens or event payload fields that contain provider secrets.

Using from .bus files

Inside a .bus file, write the module target without the bus prefix:

# same as: bus api provider events --events-backend postgres
api provider events --events-backend postgres