openvibers/openvibe.events Commits
Let OpenVibe.Bot publish its bot. events as a known source (#3)
Changes
@@@ -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."]}@@@ -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.@@@ -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}.`]]));@@@ -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);
@@ -42,7 +42,7 @@ EVENTS_RECEIPT_RETENTION_DAYS=90
@@ -42,7 +42,7 @@ EVENTS_RECEIPT_RETENTION_DAYS=90
#EVENTS_MAX_HOPS=8
#EVENTS_MAX_HOPS=8
# Which source may publish which event_type prefixes. JSON; replaces the default, which is
# 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
# { "<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.
# must start with "<source>."; "app" and "app-*" are reserved for developer apps.
#EVENTS_SOURCE_PREFIXES={"live":["live."],"media":["media."]}
#EVENTS_SOURCE_PREFIXES={"live":["live."],"media":["media."]}
@@ -73,7 +73,7 @@ delivery rollup, for Network; never visible to apps).
@@ -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).
`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`).
- 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`.
- 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.
- 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.
@@ -19,7 +19,9 @@ const list = (v, d) => (v == null || v === '' ? d : String(v).split(',').map(s =
@@ -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',
const DEFAULT_SOURCES = ['live', 'media', 'network', 'community', 'chat', 'openre', 'billing', 'tips',
'vip', 'ai', 'games', 'tools', 'codes', 'host', 'sources', 'search',
'vip', 'ai', 'games', 'tools', 'codes', 'host', 'sources', 'search',
// Publication products (Waves 16-19): each publishes <product>.<type>.<action> via openvibe-publishing.
// 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) {
function sourcePrefixes(env) {
let map = Object.fromEntries(DEFAULT_SOURCES.map(s => [s, [`${s}.`]]));
let map = Object.fromEntries(DEFAULT_SOURCES.map(s => [s, [`${s}.`]]));
@@ -75,6 +75,15 @@ t('event_type must use a prefix the source owns', async () => {
@@ -75,6 +75,15 @@ t('event_type must use a prefix the source owns', async () => {
assert.strictEqual(r.body.code, 'events.type_not_allowed');
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 () => {
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') });
const r = await request(h.base, 'POST', '/api/v1/events', { token: serviceToken('stranger', ['events.event.publish']), body: envelope('stranger') });
assert.strictEqual(r.status, 403);
assert.strictEqual(r.status, 403);