bus-api-provider-events — Bus Events API provider
bus-api-provider-events — Bus Events API provider
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. Functional providers remain event-oriented and do not implement
HTTP controllers themselves.
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 domain scopes, but event access is still account- and scope-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_EVENTS_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 scopes 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 domain scopes such as llm:proxy, container:run, or billing:read
instead of broad event scopes whenever the event namespace is protected.
The account is derived from JWT sub. Callers do not provide account IDs for
authorization.
Unprotected event names require events:send to publish and events:listen
to stream.
The provider stamps the event account from the JWT. Caller-supplied account metadata is not trusted for authorization. Streams only return events the token is allowed to receive, and user tokens cannot subscribe to unrelated accounts.
Protected Bus integration events use domain scopes. 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.
bus work events use the protected bus.work.* namespace. Those events are
generic durable work streams and require dedicated scopes such as work:send,
work:read, work:reply, work:claim, and work:admin instead of broad
event scopes. Development task events use the separate bus.dev.task.*
namespace with concrete scopes such as dev:task:send, dev:task:read,
dev:task:reply, and dev:task:claim. Future deployments may further qualify
these scopes by owner or repository, for example work:claim:acme/payroll or
dev:task:claim:busdk/bus-ledger.
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 functional providers and HTTP controllers do
not change when the backend changes.
POST /api/v1/events
Publishes one event.
The provider stamps the event account from the JWT. Caller-supplied account
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. Work delivery is selected
on stream URLs with delivery=work 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 domain scope 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=work&group=<group>&consumer=<consumer>
Streams matching events as competing work.
Use work 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.
Work 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 work 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 domain scopes 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 scope 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=work
Delivers each event to one authorized worker in the selected group.
group=<group>
Selects the work-delivery group.
Workers in the same group compete for events. Workers in different groups each receive their own group delivery.
consumer=<consumer>
Identifies one work-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 scopes include the domain scopes 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/scope 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 work-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 domain scopes needed for the events they send and receive. Do not log bearer tokens or event payload fields that contain provider secrets.