openvibers/openvibe.tips Commits

openvibe-contracts v0.30.2: the tips.* event payload schemas; test/contracts.test.js validates every envelope and payload Tips produces against them

OP OpenVibers committed 23 Sept 2026 · e042cba803 · 5 files, +68 −12


Changes

ddiff --git a/README.md b/README.md
iindex 983858f..89225d3 100644
---- a/README.md
++++ b/README.md
@@@ -41,7 +41,7 @@ from *interaction delivered*.
(openvibe-sdk transactional outbox)
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
--- **OpenVibe.Shared** v1.2.1 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
++- **OpenVibe.Shared** v1.3.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
the Network's `navbar.js`
## Run it
@@@ -50,7 +50,7 @@ from *interaction delivered*.
fnm exec --using=22.22.1 npm install
cp .env.example .env # OV_OAUTH_CLIENT_SECRET, TIPS_FORM_SECRET, TIPS_EVENTS_SECRET, …
npm run dev # http://localhost:4610
--npm test # 7 files: stub Network/Billing/Events/Live, temp DBs, random ports
++npm test # every test/*.test.js: stub Network/Billing/Events/Live, temp DBs, random ports
npm run subscribe # create the billing.transaction.* and billing.receipt.* subscriptions in OpenVibe.Events
node scripts/import-live.js --live-db <live snapshot> --billing-db <billing snapshot> [--dry-run] [--json]
```
@@@ -125,12 +125,13 @@ token as a Bearer and act on their own things only. The API never reads cookies.
| `POST /simulate` | `tips.simulation.run` | owner |
| `POST /internal/events` | Events webhook signature (`TIPS_EVENTS_SECRET`), loopback only | — |
--The capabilities and the service manifest are released in openvibe-contracts v0.15.0 (3-segment ids:
++The capabilities and the service manifest were released in openvibe-contracts v0.15.0 (3-segment ids:
the charter's `tips.simulate` is `tips.simulation.run`; `tips.interaction.record` is new, for EXTERNAL
tips); the drafts stay in [docs/capabilities-proposal/](docs/capabilities-proposal/) and
[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). Grants are matched with
--contracts' `capabilities.grants()` (exact id or a `.*` family). Contracts has no `tips.*` event payload
--schemas yet.
++contracts' `capabilities.grants()` (exact id or a `.*` family). Tips pins openvibe-contracts v0.30.2,
++which also carries the payload schemas of the six `tips.*` events below; `test/contracts.test.js`
++validates every envelope and payload Tips produces against them.
## Events
@@@ -241,7 +242,7 @@ exist here (plan §12.12):
4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing and
Events (Billing is in shadow and has sent no events; the production database is empty; the Live
import has not been run);
--5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0 (no event payload schemas yet);
++5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 (pinned);
6. a migration/seed strategy ✔, a security/threat review (not done beyond the tests' refusals), and
sitemap/robots ✔ (no feed);
7. acceptance tests proving the advertised functionality — ✔ (table above).
ddiff --git a/STATUS.json b/STATUS.json
iindex a4b7e45..50b05ef 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,6 +7,6 @@
"adr": "ADR-012",
"authoritative": false,
"deployment": "Deployed internally, not launched: openvibe-ovh /opt/openvibe.tips (release 435f32b), unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscription billing.transaction.* -> /internal/events; TIPS_CHAT_ADAPTER=none. Pins openvibe-contracts v0.15.0, openvibe-sdk v0.4.0, openvibe-shared v1.2.1.",
-- "notes": "Runtime, API, SSR pages, overlays, Billing settlement consumer, events outbox, Live import and reconciliation are built and tested against stubs. Capabilities (17 tips.*) and the service manifest are registered in OpenVibe.Contracts v0.15.0; no tips.* event payload schemas yet. Production database is empty (0 profiles, interactions, goals, overlay tokens); the Live import (scripts/import-live.js) has never been run; 0 billing events have ever arrived because Billing runs in shadow. Live's /internal/tips/deliveries route is deployed (Live f11f809) but never called. The restore drill passed on the host 2026-09-23 05:23 UTC. Built since (not deployed yet): /metrics (loopback only) and the Billing/Events checks in /api/ready. Not done: privacy/moderation controls on the pages; an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
++ "notes": "Runtime, API, SSR pages, overlays, Billing settlement consumer, events outbox, Live import and reconciliation are built and tested against stubs. Capabilities (17 tips.*) and the service manifest are registered in OpenVibe.Contracts v0.15.0; the six tips.* event payload schemas are in v0.30.2, which the repository pins since 2026-09-23 (production still runs v0.15.0). Production database is empty (0 profiles, interactions, goals, overlay tokens); the Live import (scripts/import-live.js) has never been run; 0 billing events have ever arrived because Billing runs in shadow. Live's /internal/tips/deliveries route is deployed (Live f11f809) but never called. The restore drill passed on the host 2026-09-23 05:23 UTC. Built since (not deployed yet): /metrics (loopback only) and the Billing/Events checks in /api/ready. Not done: privacy/moderation controls on the pages; an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
"updated": "2026-09-23"
}
ddiff --git a/package-lock.json b/package-lock.json
iindex d37a98a..91f8318 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -14,7 +14,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
},
@@@ -983,9 +983,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.15.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
-- "integrity": "sha512-1+Y7Qm+eplVEwxACsga3eASmBrTNF2hQ/fmw33CQtzUZuTkA12sv93LCZo3lbvFyKbbZFZDYIHSt7gfu3QISlQ==",
++ "version": "0.30.2",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
++ "integrity": "sha512-R01NgRY5V/GEYWkWUyxfl1MifSQb4dfJy11VS9RP+wsWedh2Crr9/IL7yGSn1PytwTKKL+tQl/r4ppGfH3TwLg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 1221531..d9a18b0 100644
---- a/package.json
++++ b/package.json
@@@ -21,7 +21,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
},
ddiff --git a/test/contracts.test.js b/test/contracts.test.js
nnew file mode 100644
iindex 0000000..e0f2771
---- /dev/null
++++ b/test/contracts.test.js
@@@ -0,0 +1,55 @@
++'use strict';
++// Every event Tips puts in its outbox is a valid events.event-envelope@1 whose payload validates
++// against the tips.* payload schema released in openvibe-contracts (v0.30.2 registers all six).
++const assert = require('assert');
++const contracts = require('openvibe-contracts');
++const { boot, check, done } = require('./helpers/app');
++
++const TYPES = ['tips.interaction.ready', 'tips.interaction.failed', 'tips.interaction.cancelled', 'tips.goal.updated', 'tips.overlay.delivered', 'tips.overlay.failed'];
++
++(async () => {
++ const t = await boot({ live: true });
++ const { domain, billing, live } = t;
++ const alex = await t.creator('alex', { settings: { tts_enabled: true } });
++ const viewer = t.network.newUser('viewer');
++ billing.fund(viewer.subject, 10_000);
++
++ await check('the six tips.* payload schemas are in the installed contracts', async () => {
++ for (const type of TYPES) assert.strictEqual(contracts.resolve(type).owner, 'tips', type);
++ });
++
++ await check('a flow that produces every tips.* event: each envelope and payload validates', async () => {
++ await t.call('POST', '/api/v1/goals', { user: alex, body: { title: 'Desk', target_amount: 1000 } });
++ const tok = await t.call('POST', '/api/v1/overlay-tokens', { user: alex, body: { scopes: ['alerts', 'goals'] } });
++ const s = await t.sse(`/overlay/${tok.json.secret}/events`);
++ const tip = await t.call('POST', '/api/v1/checkout', { user: viewer, body: { creator: 'alex', amount: 300, message: 'hi' } });
++ await s.waitFor((e) => e.event === 'alert' && e.data.interaction_id === tip.json.interaction.id);
++ s.close();
++ await domain.effects.drain();
++ // A delivery Live refuses (tips.interaction.failed), an overlay nobody showed (tips.overlay.failed).
++ live.state.fail = 422;
++ await t.call('POST', '/api/v1/checkout', { user: viewer, body: { creator: 'alex', amount: 5 } });
++ await domain.effects.drain();
++ live.state.fail = null;
++ t.clock.offset += 11 * 60 * 1000;
++ domain.overlays.sweepFailed();
++ t.clock.offset = 0;
++ // A reversal before delivery (tips.interaction.cancelled) and one after it.
++ const pending = await t.call('POST', '/api/v1/tts-requests', { user: viewer, body: { creator: 'alex', amount: 150, tts: { text: 'read me' } } });
++ const txn = domain.interactions.get(pending.json.interaction.id).billing_txn_id;
++ await t.deliver(billing.refund(txn));
++ await t.deliver(billing.refund(domain.interactions.get(tip.json.interaction.id).billing_txn_id, 100));
++
++ const rows = t.outboxRows();
++ for (const type of TYPES) assert.ok(rows.some((e) => e.event_type === type), `no ${type} was produced`);
++ for (const e of rows) {
++ const env = contracts.validate('events.event-envelope@1', e);
++ assert.ok(env.valid, `${e.event_type} envelope: ${JSON.stringify(env.errors)}`);
++ const p = contracts.validate(e.event_type, e.payload);
++ assert.ok(p.valid, `${e.event_type} payload: ${JSON.stringify(p.errors)} in ${JSON.stringify(e.payload)}`);
++ }
++ });
++
++ await t.close();
++ done();
++})().catch((e) => { console.error(e); process.exit(1); });