openvibers/openvibe.vip Commits

VIP on the service kit (plan T1): the outbox is openvibe-sdk/events createServiceOutbox (callers emitIn inside the change's transaction). Pins: openvibe-sdk v0.25.0, openvibe-contracts v0.79.0, openvibe-shared v2.0.0. Both suites green (verified).

OP OpenVibers committed 29 Sept 2026 · 323872f310 · 6 files, +51 −84


Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex 34ecc35..346edb4 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"deployed": true,
"deployment": "Deployed internally, not launched: deployed with `sudo ovhost deploy vip`, /opt/openvibe.vip, unit openvibe-vip, 127.0.0.1:4620 loopback only; https://openvibe.vip still serves the OpenVibe.Sites placeholder. Principal vip; three Events subscriptions for Billing's events (secret rotated 2026-09-24); checkout closed (VIP_CHECKOUT_PROVIDERS empty). On 2026-09-24 production held the Live import: 44 creators, 43 plans (one per creator), 0 memberships while Billing runs in shadow.",
-- "contracts": "openvibe-contracts v0.76.0",
++ "contracts": "openvibe-contracts v0.79.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"roadmap": "Wave 10",
"works": [
@@@ -20,7 +20,7 @@
"reusable membership card (GET /embed/:username/card.json, GET /api/v1/creators/:ref/card), badge (/embed/:username/badge.svg) and embeddable widget (/embed/:username/widget): public data only, no cookies, the widget's CSP frame-ancestors configurable (VIP_WIDGET_FRAME_ANCESTORS), member count from Billing that the creator can hide (not deployed yet)",
"the Live subscription import has run: 43 plans and plan versions, 44 creators, 87 migration maps (production, 2026-09-23)",
"Chat (subscriber badge, vip.entitlement.check), Community (members-only spaces and threads) and Blog (members-only posts) call VIP in production (vip.resource.policy.evaluate grants for Community and Blog)",
-- "14 vip.* capabilities, the service manifest and its events registered in openvibe-contracts v0.19.0 (this repository pins v0.49.0)",
++ "14 vip.* capabilities, the service manifest and its events registered in openvibe-contracts v0.19.0 (this repository pins v0.79.0)",
"per-actor limits on /api/v1 (openvibe-sdk/limits)"
],
"not_yet": [
ddiff --git a/package-lock.json b/package-lock.json
iindex d8a5292..9b61fa7 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -14,9 +14,9 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.20.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.27.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
"pg": "^8.23.0"
},
"devDependencies": {
@@@ -1117,9 +1117,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.76.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
-- "integrity": "sha512-zTdx776KWrcJKm1JTnf+0+BnsvswdvXy1ldPyse1hSqhnW/SSr98sX696eRetFUhTl8qVM1xSRCykqIUbqItPw==",
++ "version": "0.79.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
++ "integrity": "sha512-FY5GLbiOq0kDw5lUPPzX6h3ATbzvsr/mX5DUCF4GRfn6vGMP0p89rZZDhgtVDaS+yCBCYMZ8zFaZxuHEkEVwVg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
@@@ -1133,9 +1133,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.20.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.20.1",
-- "integrity": "sha512-3Zwz8sJhWhnuw9GYr6ieIDp9CIl6ug0QqJE5ifDsk4ckrethBjcDzjadE1GSCGQDFXXLf/+bVWtI7X3VOVXmFQ==",
++ "version": "0.25.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
++ "integrity": "sha512-8eurZLyFUtB8XN16hLUuYkah+qwS3tkvdzul5qdZkJTi0aZ4q8dq2QGWSq6pN5mtVPdJyuJ8Mo53r36lixmP1g==",
"license": "MIT",
"engines": {
"node": ">=20"
@@@ -1162,9 +1162,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.27.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.27.0",
-- "integrity": "sha512-/H7dyFw+cURcdjyfbpyY7mNc6F+Pcb9TwkVwZHR+9d3M62q70KDhZLo/gZi71eaYxUjD8lO8tmbXu70EAa9paQ==",
++ "version": "2.0.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
++ "integrity": "sha512-n5CHC0hfxOo0jo/3kS4O3KiwhirhZDlx5zZTIuPyIf3Dq0yI/R6R64btx7NYwZe5dI0NjvqzEtnv19m8unwLMg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 9a67ce6..b90b0f1 100644
---- a/package.json
++++ b/package.json
@@@ -31,9 +31,9 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.20.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.27.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
"pg": "^8.23.0"
},
"engines": {
ddiff --git a/server/domain/entitlements.js b/server/domain/entitlements.js
iindex 3a60c44..a9063ca 100644
---- a/server/domain/entitlements.js
++++ b/server/domain/entitlements.js
@@@ -35,14 +35,20 @@ function createEntitlements({ db, now, config, billing, outbox, memberships, pla
async function emitChanged(row, prev, { reason, membership }, traceparent) {
const v = membership && membership.plan_version_id ? await plans.version(membership.plan_version_id) : null;
-- await outbox.emit('vip.membership.changed', { type: 'membership', id: `${row.member_subject}:${row.creator_subject}` }, {
-- member: userRef(row.member_subject), creator: userRef(row.creator_subject), kind: row.kind,
-- active: !!row.active, expires_at: row.expires_at, cancel_at_period_end: !!row.cancel_at_period_end,
-- previous: prev ? { active: !!prev.active, expires_at: prev.expires_at, cancel_at_period_end: !!prev.cancel_at_period_end } : null,
-- reason: reason || null, source: row.source, billing_event_id: row.billing_event_id || null,
-- membership_id: membership ? membership.id : null, plan_id: membership ? membership.plan_id : null,
-- plan_version_id: membership ? membership.plan_version_id : null, plan_version: v ? v.version : null,
-- subscription_id: row.subscription_id || null,
++ await outbox.emit({
++ event_type: 'vip.membership.changed',
++ actor: { type: 'service', id: 'vip' },
++ subject: { type: 'membership', id: `${row.member_subject}:${row.creator_subject}` },
++ visibility: 'internal', priority: 'important',
++ payload: {
++ member: userRef(row.member_subject), creator: userRef(row.creator_subject), kind: row.kind,
++ active: !!row.active, expires_at: row.expires_at, cancel_at_period_end: !!row.cancel_at_period_end,
++ previous: prev ? { active: !!prev.active, expires_at: prev.expires_at, cancel_at_period_end: !!prev.cancel_at_period_end } : null,
++ reason: reason || null, source: row.source, billing_event_id: row.billing_event_id || null,
++ membership_id: membership ? membership.id : null, plan_id: membership ? membership.plan_id : null,
++ plan_version_id: membership ? membership.plan_version_id : null, plan_version: v ? v.version : null,
++ subscription_id: row.subscription_id || null,
++ },
}, { traceparent });
}
ddiff --git a/server/domain/plans.js b/server/domain/plans.js
iindex fa6f2f2..8486bb1 100644
---- a/server/domain/plans.js
++++ b/server/domain/plans.js
@@@ -53,11 +53,17 @@ function createPlans({ db, now, outbox, creators, perks }) {
async function emitPublished(plan, v, traceparent) {
const creator = await creators.byId(plan.creator_id);
-- await outbox.emit('vip.plan.published', { type: 'plan', id: plan.id, revision: v.version }, {
-- plan_id: plan.id, slug: plan.slug, version: v.version, version_id: v.id,
-- creator: creators.present(creator), billing_kind: plan.billing_kind,
-- terms: json(v.terms, {}), published_at: v.published_at,
-- }, { visibility: 'public', traceparent });
++ await outbox.emit({
++ event_type: 'vip.plan.published',
++ actor: { type: 'service', id: 'vip' },
++ subject: { type: 'plan', id: plan.id, revision: v.version },
++ visibility: 'public', priority: 'important',
++ payload: {
++ plan_id: plan.id, slug: plan.slug, version: v.version, version_id: v.id,
++ creator: creators.present(creator), billing_kind: plan.billing_kind,
++ terms: json(v.terms, {}), published_at: v.published_at,
++ },
++ }, { traceparent });
}
function publishConflict(e) {
ddiff --git a/server/events/outbox.js b/server/events/outbox.js
iindex df409a1..a1ff630 100644
---- a/server/events/outbox.js
++++ b/server/events/outbox.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * VIP → OpenVibe.Events through the openvibe-sdk transactional outbox (ADR-004).
++ * VIP → OpenVibe.Events through openvibe-sdk/events createServiceOutbox (ADR-004).
*
* vip.plan.published a plan version became what new members buy (publish, or an edit of a
* published plan); payload carries the version's full terms
@@@ -15,57 +15,12 @@
* EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise rows wait in event_outbox.
*/
const { validate } = require('openvibe-contracts');
--const { createClient } = require('openvibe-sdk/core');
--const { createServiceTokenClient } = require('openvibe-sdk/auth');
--const { createEventsClient, createPgOutbox } = require('openvibe-sdk/events');
++const { createServiceOutbox } = require('openvibe-sdk/events');
--const ACTOR = { type: 'service', id: 'vip' };
--
--function createVipOutbox({ db, config, fetchImpl, now = () => Date.now(), log = console }) {
-- const enabled = !!(config.events.url && config.oauth.clientSecret);
-- const clientOpts = { baseUrls: { events: config.events.url || 'http://127.0.0.1:4300' }, retries: 0 };
-- if (fetchImpl) clientOpts.fetch = fetchImpl;
-- if (enabled) {
-- clientOpts.tokenProvider = createServiceTokenClient({
-- tokenUrl: `${config.network.internalUrl}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
-- scope: { 'openvibe.events': 'events.event.publish' }, ...(fetchImpl ? { fetch: fetchImpl } : {}),
-- });
-- } else {
-- clientOpts.getToken = async () => { throw new Error('events relay disabled (EVENTS_URL / OV_OAUTH_CLIENT_SECRET unset)'); };
-- }
-- const events = createEventsClient(createClient(clientOpts), { source: 'vip' });
-- let lastError = null;
-- // The PostgreSQL outbox: rows are written in the change's own transaction (enqueue(db, …) joins the ambient
-- // transaction); several processes relay one table safely (leases).
-- const outbox = createPgOutbox(db, {
-- events,
-- intervalMs: config.events.intervalMs,
-- now,
-- onError: (err) => {
-- const msg = err && err.message;
-- if (msg !== lastError) log.warn('[VIP] event publish failed (will retry):', msg);
-- lastError = msg;
-- },
-- });
--
-- /** Inside the caller's transaction. */
-- function emit(eventType, subject, payload, { visibility = 'internal', priority = 'important', traceparent } = {}) {
-- const env = events.prepare({ event_type: eventType, actor: ACTOR, subject, payload, visibility, priority }, { now: now() });
-- const v = validate('events.event-envelope@1', env);
-- if (!v.valid) throw new Error(`outbox: invalid envelope for ${eventType}: ${v.errors.map((e) => `${e.path} ${e.message}`).join('; ')}`);
-- return outbox.enqueue(db, env, { traceparent });
-- }
--
-- return {
-- emit,
-- outbox,
-- enabled,
-- start() { if (enabled) outbox.start(); },
-- stop: () => outbox.stop(),
-- kick() { if (enabled) outbox.kick(); },
-- // pending()/rejected() are queries on PostgreSQL: awaited, or the health output serialized two Promises as {}.
-- status: async () => ({ enabled, pending: await outbox.pending(), rejected: await outbox.rejected(), last_error: lastError }),
-- };
--}
--
--module.exports = { createVipOutbox };
++module.exports = {
++ createVipOutbox: ({ db, config, fetchImpl, now, log }) => createServiceOutbox({
++ db, source: 'vip', eventsUrl: config.events.url, networkInternalUrl: config.network.internalUrl,
++ clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret, intervalMs: config.events.intervalMs,
++ now, fetch: fetchImpl, log, validate: (env) => validate('events.event-envelope@1', env),
++ }),
++};