openvibers/openvibe.events Commits

Let OpenVibe.Bot publish its bot. events as a known source (#3)

OP OpenVibers committed 2 Oct 2026 · 18a9a3c4c9 · 4 files, +14 −3

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by Codex GPT-6 Sol

https://github.com/OpenVibers/OpenVibe.Events/pull/3

Changes

ddiff --git a/.env.example b/.env.example
iindex 04f3717..ae4b632 100644
---- a/.env.example
++++ b/.env.example
@@@ -42,7 +42,7 @@ EVENTS_RECEIPT_RETENTION_DAYS=90
#EVENTS_MAX_HOPS=8
# Which source may publish which event_type prefixes. JSON; replaces the default, which is
# { "<source>": ["<source>."] } for live media network community chat openre billing tips vip
--# ai games tools codes host sources search wiki blog news reviews deals coupons trade. Every prefix
++# ai games tools codes host sources search wiki blog news reviews deals coupons trade bot. Every prefix
# must start with "<source>."; "app" and "app-*" are reserved for developer apps.
#EVENTS_SOURCE_PREFIXES={"live":["live."],"media":["media."]}
ddiff --git a/README.md b/README.md
iindex 3982d2d..11efac6 100644
---- a/README.md
++++ b/README.md
@@@ -73,7 +73,7 @@ delivery rollup, for Network; never visible to apps).
`POST /api/v1/events` takes one `events.event-envelope@1` envelope, or `{ "events": [ … ] }` (up to 100, stored atomically).
--- `source` must be the calling service (`svc:live` publishes `source: "live"`), and `event_type` must start with a prefix that source owns (`live.*`, `media.*`, `network.*`, `community.*`, `chat.*`, `openre.*`, `billing.*`, `tips.*`, `vip.*`, `ai.*`, `games.*`, `tools.*`, `codes.*`, `host.*`; `EVENTS_SOURCE_PREFIXES` overrides).
++- `source` must be the calling service (`svc:live` publishes `source: "live"`), and `event_type` must start with a prefix that source owns (`live.*`, `media.*`, `network.*`, `community.*`, `chat.*`, `openre.*`, `billing.*`, `tips.*`, `vip.*`, `ai.*`, `games.*`, `tools.*`, `codes.*`, `host.*`, `bot.*`; `EVENTS_SOURCE_PREFIXES` overrides).
- Idempotent on `event_id`: a repeat answers `200 { event_id, seq, duplicate: true }` and is never stored twice (even after retention, for `EVENTS_RECEIPT_RETENTION_DAYS`).
- Missing `trace_id` is taken from the request's `traceparent`; `priority` defaults to `important`, `visibility` to `internal`.
- Loop guard: an event whose trace already carries 8 hops (the cross-service chain depth, or the same source/type/subject repeating in the trace) is refused with `409 events.loop_detected`. The chain depth of a first-party publish counts first-party events only (developer-app events in the same trace never count toward it), so an app cannot poison a trace it has seen.
ddiff --git a/server/config.js b/server/config.js
iindex a2cdef4..c5c6a7d 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -19,7 +19,9 @@ const list = (v, d) => (v == null || v === '' ? d : String(v).split(',').map(s =
const DEFAULT_SOURCES = ['live', 'media', 'network', 'community', 'chat', 'openre', 'billing', 'tips',
'vip', 'ai', 'games', 'tools', 'codes', 'host', 'sources', 'search',
// Publication products (Waves 16-19): each publishes <product>.<type>.<action> via openvibe-publishing.
-- 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade'];
++ 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade',
++ // Robot control (OpenVibe.Bot): bot.robot.*, bot.estop.*, bot.command.*
++ 'bot'];
function sourcePrefixes(env) {
let map = Object.fromEntries(DEFAULT_SOURCES.map(s => [s, [`${s}.`]]));
ddiff --git a/test/publish.test.js b/test/publish.test.js
iindex c01c1ff..6fead63 100644
---- a/test/publish.test.js
++++ b/test/publish.test.js
@@@ -75,6 +75,15 @@ t('event_type must use a prefix the source owns', async () => {
assert.strictEqual(r.body.code, 'events.type_not_allowed');
});
++t('a bot service principal publishes bot.* only', async () => {
++ const bot = serviceToken('bot', ['events.event.publish']);
++ let r = await request(h.base, 'POST', '/api/v1/events', { token: bot, body: envelope('bot', { event_type: 'bot.robot.online' }) });
++ assert.strictEqual(r.status, 201, r.text);
++ r = await request(h.base, 'POST', '/api/v1/events', { token: bot, body: envelope('bot', { event_type: 'live.stream.started' }) });
++ assert.strictEqual(r.status, 403);
++ assert.strictEqual(r.body.code, 'events.type_not_allowed');
++});
++
t('unknown source (not in the manifest map) -> 403', async () => {
const r = await request(h.base, 'POST', '/api/v1/events', { token: serviceToken('stranger', ['events.event.publish']), body: envelope('stranger') });
assert.strictEqual(r.status, 403);