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).
Changes
@@@ -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": [@@@ -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"@@@ -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": {@@@ -35,14 +35,20 @@ function createEntitlements({ db, now, config, billing, outbox, memberships, plaasync 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 });}@@@ -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) {@@@ -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),++ }),++};
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"deployed": 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.",
"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)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"roadmap": "Wave 10",
"roadmap": "Wave 10",
"works": [
"works": [
@@ -20,7 +20,7 @@
@@ -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)",
"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)",
"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)",
"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)"
"per-actor limits on /api/v1 (openvibe-sdk/limits)"
],
],
"not_yet": [
"not_yet": [
@@ -14,9 +14,9 @@
@@ -14,9 +14,9 @@
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
"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"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1117,9 +1117,9 @@
@@ -1117,9 +1117,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1133,9 +1133,9 @@
@@ -1133,9 +1133,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"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",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -1162,9 +1162,9 @@
@@ -1162,9 +1162,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -31,9 +31,9 @@
@@ -31,9 +31,9 @@
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
"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"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -35,14 +35,20 @@ function createEntitlements({ db, now, config, billing, outbox, memberships, pla
@@ -35,14 +35,20 @@ function createEntitlements({ db, now, config, billing, outbox, memberships, pla
async function emitChanged(row, prev, { reason, membership }, traceparent) {
async function emitChanged(row, prev, { reason, membership }, traceparent) {
const v = membership && membership.plan_version_id ? await plans.version(membership.plan_version_id) : null;
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 });
}, { traceparent });
}
}
@@ -53,11 +53,17 @@ function createPlans({ db, now, outbox, creators, perks }) {
@@ -53,11 +53,17 @@ function createPlans({ db, now, outbox, creators, perks }) {
async function emitPublished(plan, v, traceparent) {
async function emitPublished(plan, v, traceparent) {
const creator = await creators.byId(plan.creator_id);
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) {
function publishConflict(e) {
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'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
* 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
* published plan); payload carries the version's full terms
@@ -15,57 +15,12 @@
@@ -15,57 +15,12 @@
* EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise rows wait in event_outbox.
* EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise rows wait in event_outbox.
*/
*/
const { validate } = require('openvibe-contracts');
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),
+ }),
+};