openvibers/openvibe.examples Commits
examples/vibes-pay: a tiny shop that sells a sticker pack for Vibes (#56)
Changes
@@@ -3,13 +3,13 @@> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials> and public APIs, nothing else.--**Status:** alpha, 0.3.0 (roadmap Wave 20; the `create-openvibe-app` scaffolder, WS-N task 6). Fifteen examples, each with a smoke test that CI runs on++**Status:** alpha, 0.3.0 (roadmap Wave 20; the `create-openvibe-app` scaffolder, WS-N task 6). Eighteen examples, each with a smoke test that CI runs onNode 22.22.1 against `openvibe-sdk/testing`'s mock platform (no network). `npm run e2e` runs threeof them (Media, Events pull, a Tools job) against the real platform with your own sandbox app; itis not part of CI or `npm test`, and it was not run against production as part of this release.`npm run developer-path` is the Wave 20 exit check (account to Media, Events and AI to revokedcredentials); CI runs it against the mock platform, and OpenVibe.Host runs it against production daily.--**Built on:** [openvibe-sdk v0.82.0](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.62.0), and++**Built on:** [openvibe-sdk v0.84.0](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.84.0), andopenvibe-contracts v0.222.0 for the mod manifest.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.1and §18.9; roadmap Wave 20, §30 (public SDK surface), ADR-014 (developer projects).@@@ -42,6 +42,7 @@ Each example is small, runs on its own and proves one thing with a smoke test.| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` || [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` || [ai-anthropic](examples/ai-anthropic) | The official `@anthropic-ai/sdk` client on `https://ai.openvibe.services` (POST /v1/messages) with a Network app token through the SDK's `credentials`: the answer names the model that ran and carries `x_openvibe`. |++| [vibes-pay](examples/vibes-pay) | Sell a sticker pack for Vibes: the server creates a charge with the SDK (the order id is the idempotency key), the page opens the pay sheet with `vibes-pay.js`, and a signed webhook marks the order paid once. | `openvibe-sdk/vibes` (`charges.create`, `webhooks.verify`), `openvibe-shared/vibes-pay.js` (`OpenVibe.vibes.pay`, `[data-ov-vibes-pay]`) || [ai-python](examples/ai-python) | The official `openai` Python client on `https://ai.openvibe.services/v1`, with a Network app token from client credentials kept and renewed a minute early. |Each example folder has a README, a `.env.example`, its own `package.json` and a@@@ -132,6 +133,7 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.| [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` || [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` || [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |++| [vibes-pay](examples/vibes-pay) | confidential | `billing.vibes.charge` | `OV_APP_TOKEN`, `OV_VIBES_WEBHOOK_SECRET`, `OV_BILLING_URL`, `OV_PUBLIC_URL`, `OV_ITEM_NAME`, `OV_PRICE_VIBES`, `OV_PORT` || [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with@@@ -367,5 +369,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [<!-- versions:start -->- openvibe-contracts: v0.222.0--- openvibe-sdk: v0.82.0++- openvibe-sdk: v0.84.0<!-- versions:end -->@@@ -5,11 +5,11 @@"wave": "W20","version": "0.3.0","contracts": "openvibe-contracts v0.222.0 (mod-manifest example)",-- "sdk": "openvibe-sdk v0.82.0",++ "sdk": "openvibe-sdk v0.84.0","deployed": false,"deployment": "none: a repository with CI, not a runtime and not a published package","features": [-- "fifteen copyable examples: browser app, Node server app, Media uploader, event subscriber, webhook consumer, tool job, OAuth app, chat bot, mod manifest, AI chat (the official openai client on ai.openvibe.services/v1, plain, streamed and the project's own provider), go live (a Live API token sets up a WHIP slot; FFmpeg 8+ publishes; the session is named and ended), Zone over S3 (the official AWS SDK v3: put, get, paged list, presigned share link, delete), Space deploys from CI (an app token, live or preview, rollback, a GitHub Actions workflow), what people wear (Inventory's public catalog and equipped reads, no token), game scores (a sandbox- title on OpenVibe.Play: a round counted once, an unlock, the public board)",++ "eighteen copyable examples: browser app, Node server app, Media uploader, event subscriber, webhook consumer, tool job, OAuth app, chat bot, mod manifest, AI chat (the official openai client on ai.openvibe.services/v1, plain, streamed and the project's own provider), go live (a Live API token sets up a WHIP slot; FFmpeg 8+ publishes; the session is named and ended), Zone over S3 (the official AWS SDK v3: put, get, paged list, presigned share link, delete), Space deploys from CI (an app token, live or preview, rollback, a GitHub Actions workflow), what people wear (Inventory's public catalog and equipped reads, no token), game scores (a sandbox- title on OpenVibe.Play: a round counted once, an unlock, the public board), Vibes Pay (a tiny sticker shop: a charge with openvibe-sdk/vibes, vibes-pay.js, and a signed webhook that marks the order paid)","smoke tests against openvibe-sdk/testing's mock platform, offline","create-openvibe-app scaffolder","npm run e2e against the real platform (on demand)",@@@ -14,6 +14,6 @@},"dependencies": {"@anthropic-ai/sdk": "1.4.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -14,6 +14,6 @@},"dependencies": {"openai": "7.42.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,7 +13,7 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -13,6 +13,6 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}nnew file mode 100644@@@ -0,0 +1,14 @@++# The app's token (capability billing.vibes.charge). Keep it on the server; never put it in the page.++OV_APP_TOKEN=++++# The app's webhook secret from its page in OpenVibe.Services. Keep it on the server.++OV_VIBES_WEBHOOK_SECRET=++++# Billing's origin and this shop's public origin (the return_url is built from it).++OV_BILLING_URL=https://billing.openvibe.network++OV_PUBLIC_URL=http://localhost:8099++++# The one item in this tiny shop.++OV_PORT=8099++OV_ITEM_NAME=Sticker pack: Neon cats++OV_PRICE_VIBES=50nnew file mode 100644@@@ -0,0 +1,58 @@++# Vibes Pay: a tiny sticker shop++++Sell one thing for Vibes (MONEY.md, D12). The server creates the charge with `openvibe-sdk/vibes`; the page opens++OpenVibe's pay sheet with `openvibe-shared/vibes-pay.js`; a signed webhook marks the order paid. No card form is drawn++anywhere: the person pays Vibes they already have.++++```bash++cp .env.example .env # fill OV_APP_TOKEN and OV_VIBES_WEBHOOK_SECRET++npm start # http://localhost:8099++```++++The app needs `billing.vibes.charge`; make a sandbox app in your project and grant it. Set its webhook URL to your++public `/webhooks/vibes` endpoint.++++## What it proves++++- **The charge is the server's.** `GET /` calls `vibes.charges.create({ amount, description, reference,++ return_url })` with the app token. `amount` is a whole number of Vibes, and the order id is the reference, so the++ SDK sends it as the idempotency key: a retried create is the same charge, never a second one. The app token never++ reaches the page.++- **The page uses `vibes-pay.js`.** `server.js` serves the pinned `openvibe-shared` file at `/vibes-pay.js`, and++ `public/index.html` has `<button data-ov-vibes-pay="{{pay_url}}">`; the button binds itself in the Vibes accent,++ opens the sheet (a popup on a computer, a full page on a phone), and the page listens for the `ov-vibes-pay`++ result as a hint to refresh.++- **The webhook is the confirmation.** `POST /webhooks/vibes` reads the raw body, verifies++ `X-OpenVibe-Signature-V2` with `vibes.webhooks.verify`, and marks the order paid once. A bad signature is 401 and++ changes nothing; a repeat delivery is `duplicate: true`. A refund event moves the order to `refunded`.++- **The page never decides money.** `GET /api/orders/:reference` is the server's own state; the return page++ (`/thanks?order=…`) reads it too.++++The smoke test runs the shop against a local Billing stand-in, using the released `openvibe-sdk/vibes` client for the++charge and the webhook check and the pinned `openvibe-shared` `vibes-pay.js`. It needs no network and no real Vibes.++It checks the create body and idempotency key, the page's button and served `vibes-pay.js`, a refused signature, a++paid delivery, its replay, a refund and a second order.++++## Run it against the real platform++++1. Create a project and a **sandbox app** on <https://openvibe.services>, then grant the app++ `billing.vibes.charge`.++2. Put the app's token and its webhook secret in `.env` (`OV_APP_TOKEN`, `OV_VIBES_WEBHOOK_SECRET`); set++ `OV_PUBLIC_URL` to an HTTPS origin that reaches this server, because Billing posts the webhook there and the++ `return_url` is built from it.++3. `npm start`, open the shop, and pay with sandbox Vibes in the sheet.++4. Open `/api/orders/<reference>` (or watch the server log): the webhook marks it `paid`. Refund it from the app's++ server with `vibes.charges.refund(id)` and watch the refund event.++++Sandbox charges use sandbox Vibes (test grants worth nothing) and never touch a person's real Vibes. Production Vibes++Pay opens when the owner enables Billing's `billing.vibes` settings; see openvibe.services `/docs/vibes` for the++plain-HTTP flow, signatures, limits and payouts.++++## Pins++++`package.json` pins the two releases this example uses:++++- `openvibe-sdk`: **v0.84.0** (`openvibe-sdk/vibes`).++- `openvibe-shared`: **v4.111.0** (`vibes-pay.js`, served by `server.js` at `/vibes-pay.js`).++++`npm install` fetches both from their release tags; no branch tarball is used.nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "name": "openvibe-example-vibes-pay",++ "version": "0.1.0",++ "private": true,++ "description": "Vibes Pay: a tiny sticker shop that creates a charge with openvibe-sdk/vibes, opens the pay sheet with vibes-pay.js, and marks the order paid from a signed webhook.",++ "license": "MIT",++ "main": "server.js",++ "scripts": {++ "start": "node --env-file-if-exists=.env server.js",++ "test": "node test/smoke.test.js"++ },++ "engines": {++ "node": ">=22"++ },++ "dependencies": {++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.111.0"++ }++}nnew file mode 100644@@@ -0,0 +1,60 @@++<!doctype html>++<html lang="en">++<head>++ <meta charset="utf-8">++ <meta name="viewport" content="width=device-width, initial-scale=1">++ <meta name="color-scheme" content="dark light">++ <title>{{item}} — Vibes Pay shop</title>++ <link rel="stylesheet" href="/shop.css">++ <script src="/vibes-pay.js" defer></script>++</head>++<body>++<main data-order="{{reference}}">++ <p class="eyebrow">Vibes Pay shop</p>++ <h1>{{item}}</h1>++ <p class="lede">A tiny shop on OpenVibe. Pay once with Vibes; the webhook marks the order paid.</p>++ <div class="sticker">++ <div class="art" aria-hidden="true"></div>++ <div><strong>{{item}}</strong><span class="price">{{price}} Vibes</span></div>++ </div>++ <div class="pay">++ <button type="button" data-ov-vibes-pay="{{pay_url}}">Pay with Vibes</button>++ <noscript><a href="{{pay_url}}">Pay with Vibes</a></noscript>++ </div>++ <p id="status" role="status" aria-live="polite"></p>++ <p class="hint">You pay on OpenVibe's sheet with Vibes you already have. The sticker pack is delivered when the++ signed webhook says the charge is paid. This page is an example; no real stickers are shipped.</p>++</main>++<script>++ (function () {++ var main = document.querySelector('main');++ var order = main && main.getAttribute('data-order');++ var status = document.getElementById('status');++ if (!order || !status) return;++++ function show(text, kind) { status.textContent = text; status.className = kind || ''; }++++ async function check() {++ try {++ var res = await fetch('/api/orders/' + encodeURIComponent(order));++ if (!res.ok) return;++ var orderState = await res.json();++ if (orderState.status === 'paid') show('Paid — the sticker pack is yours.', 'paid');++ else if (orderState.status === 'refunded') show('Refunded to your Vibes.', '');++ else show('Waiting for the payment…');++ } catch (e) { show('Could not reach the shop. Try again.', 'error'); }++ }++++ document.addEventListener('ov-vibes-pay', function (event) {++ if (event.detail && event.detail.status === 'paid') {++ show('Paid — the sticker pack is yours.', 'paid');++ check();++ } else if (event.detail && event.detail.status === 'closed') {++ show('The sheet was closed. Nothing was charged.');++ }++ });++ check();++ }());++</script>++</body>++</html>nnew file mode 100644@@@ -0,0 +1,21 @@++/* The tiny shop's own look; the Pay button's look comes from openvibe.network/shared/vibes-pay.js. */++:root { color-scheme: dark light; --bg: #0b0f17; --card: #121826; --text: #e8ecf3; --muted: #93a0b4; --line: rgba(255,255,255,.12); }++@media (prefers-color-scheme: light) { :root { --bg: #f4f7fb; --card: #ffffff; --text: #111827; --muted: #5b6575; --line: rgba(17,24,39,.12); } }++* { box-sizing: border-box; }++body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: var(--bg); color: var(--text);++ font: 16px/1.55 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; }++main { width: min(560px, 100%); background: var(--card); border: 1px solid var(--line); border-radius: 20px; padding: clamp(22px, 5vw, 40px);++ box-shadow: 0 24px 60px -36px rgba(0,0,0,.6); }++.eyebrow { margin: 0 0 8px; color: #4ade80; font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }++h1 { margin: 0 0 8px; font-size: clamp(26px, 5vw, 34px); line-height: 1.15; }++.lede { margin: 0 0 20px; color: var(--muted); }++.sticker { display: flex; align-items: center; gap: 14px; padding: 16px; border: 1px solid var(--line); border-radius: 14px; margin: 0 0 20px; }++.sticker .art { width: 64px; height: 64px; flex: none; border-radius: 12px; background: radial-gradient(circle at 35% 30%, #86efac, #22c55e 60%, #15803d); }++.sticker strong { display: block; }++.price { color: #4ade80; font-weight: 800; }++.pay { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }++.hint { color: var(--muted); font-size: 14px; margin: 14px 0 0; }++#status { min-height: 1.5em; margin: 16px 0 0; font-weight: 600; }++#status.paid { color: #16a34a; }++#status.error { color: #dc2626; }++a { color: inherit; }nnew file mode 100644@@@ -0,0 +1,31 @@++<!doctype html>++<html lang="en">++<head>++ <meta charset="utf-8">++ <meta name="viewport" content="width=device-width, initial-scale=1">++ <title>Thanks — Vibes Pay shop</title>++ <link rel="stylesheet" href="/shop.css">++</head>++<body>++<main>++ <p class="eyebrow">Vibes Pay shop</p>++ <h1 id="thanks-title">Thanks for the Vibes</h1>++ <p class="lede" id="thanks-text">This is the return page. The shop will show the sticker pack as paid once the signed webhook confirms it.</p>++ <p><a href="/">Back to the shop</a></p>++</main>++<script>++ (function () {++ var order = new URLSearchParams(location.search).get('order');++ if (!order) return;++ var title = document.getElementById('thanks-title');++ var text = document.getElementById('thanks-text');++ fetch('/api/orders/' + encodeURIComponent(order)).then(function (r) { return r.ok ? r.json() : null; }).then(function (o) {++ if (!o) return;++ if (o.status === 'paid') { title.textContent = 'Paid — the sticker pack is yours.'; text.textContent = 'The webhook confirmed order ' + o.reference + '.'; }++ else if (o.status === 'refunded') { title.textContent = 'Refunded.'; text.textContent = 'The Vibes went back to your wallet.'; }++ else { title.textContent = 'Waiting for the payment…'; }++ }).catch(function () {});++ }());++</script>++</body>++</html>nnew file mode 100644@@@ -0,0 +1,231 @@++#!/usr/bin/env node++'use strict';++/**++ * Vibes Pay: a tiny Node shop that sells one sticker pack for whole Vibes (MONEY.md, D12).++ *++ * 1. GET / creates an order and a charge with openvibe-sdk/vibes (the app token, capability++ * billing.vibes.charge; the order id is the reference, so a retry is the same charge);++ * 2. the page opens the pay sheet with openvibe-shared/vibes-pay.js (OpenVibe.vibes.pay, or the++ * auto-bound [data-ov-vibes-pay] button);++ * 3. Billing posts billing.vibes.charge.paid to /webhooks/vibes; the server verifies the raw body++ * with the SDK and marks the order paid, once.++ *++ * The app token and the webhook secret stay here, never in the page. Run it with:++ *++ * npm start # reads .env (node --env-file-if-exists)++ * node server.js++ *++ * This example only uses public surfaces: Billing's /api/v1/vibes routes and the SDK. The smoke test++ * runs the same shop against a local Billing stand-in, never the network.++ */++const crypto = require('node:crypto');++const fs = require('node:fs');++const http = require('node:http');++const path = require('node:path');++++const PUBLIC = path.join(__dirname, 'public');++const INDEX = fs.readFileSync(path.join(PUBLIC, 'index.html'), 'utf8');++const THANKS = fs.readFileSync(path.join(PUBLIC, 'thanks.html'), 'utf8');++const CSS = fs.readFileSync(path.join(PUBLIC, 'shop.css'), 'utf8');++const VIBES_PAY = fs.readFileSync(require.resolve('openvibe-shared/vibes-pay'), 'utf8');++const MAX_BODY = 64 * 1024;++++function loadConfig(env = process.env) {++ const missing = ['OV_APP_TOKEN', 'OV_VIBES_WEBHOOK_SECRET'].filter((k) => !env[k]);++ if (missing.length) {++ throw Object.assign(new Error(`missing environment variables: ${missing.join(', ')} (see .env.example)`), { code: 'config.missing' });++ }++ const port = Number(env.OV_PORT || 8099);++ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('OV_PORT is a port number');++ const price = Number(env.OV_PRICE_VIBES || 50);++ if (!Number.isSafeInteger(price) || price <= 0) throw new Error('OV_PRICE_VIBES is a positive whole number of Vibes');++ const publicUrl = String(env.OV_PUBLIC_URL || `http://localhost:${port}`).replace(/\/+$/, '');++ let parsed;++ try { parsed = new URL(publicUrl); } catch { throw new Error('OV_PUBLIC_URL is an http(s) URL'); }++ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw new Error('OV_PUBLIC_URL is an http(s) URL');++ return {++ port,++ publicUrl,++ billingUrl: String(env.OV_BILLING_URL || 'https://billing.openvibe.network').replace(/\/+$/, ''),++ appToken: env.OV_APP_TOKEN,++ webhookSecret: env.OV_VIBES_WEBHOOK_SECRET,++ item: env.OV_ITEM_NAME || 'Sticker pack: Neon cats',++ price,++ };++}++++function escapeHtml(s) {++ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));++}++++function render(template, vars) {++ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (key in vars ? escapeHtml(vars[key]) : ''));++}++++function readBody(req, limit = MAX_BODY) {++ return new Promise((resolve, reject) => {++ const chunks = [];++ let size = 0;++ req.on('data', (chunk) => {++ size += chunk.length;++ if (size > limit) {++ const err = new Error('body too large');++ err.status = 413;++ reject(err);++ req.destroy();++ return;++ }++ chunks.push(chunk);++ });++ req.on('end', () => resolve(Buffer.concat(chunks)));++ req.on('error', reject);++ });++}++++function send(res, status, body, type = 'application/json; charset=utf-8') {++ const payload = typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body);++ res.writeHead(status, { 'Content-Type': type, 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });++ res.end(payload);++}++++/**++ * Build the shop. `vibes` is the openvibe-sdk/vibes client; the smoke test passes the released client++ * pointed at a local Billing stand-in. It must have charges.create/get/refund and webhooks.verify.++ */++function createShop({ config, vibes, logger = console, now = Date.now }) {++ if (!config || !vibes) throw new TypeError('createShop: config and vibes are required');++ if (!vibes.charges || typeof vibes.charges.create !== 'function') throw new TypeError('createShop: vibes.charges.create is required');++ if (!vibes.webhooks || typeof vibes.webhooks.verify !== 'function') throw new TypeError('createShop: vibes.webhooks.verify is required');++ const orders = new Map();++ const byCharge = new Map();++++ function reference() {++ return `order_${now().toString(36)}_${crypto.randomBytes(5).toString('hex')}`;++ }++++ async function createOrder() {++ const ref = reference();++ const order = {++ reference: ref,++ charge_id: null,++ pay_url: null,++ amount: config.price,++ item: config.item,++ status: 'creating',++ created_at: new Date(now()).toISOString(),++ paid_at: null,++ };++ orders.set(ref, order);++ try {++ const charge = await vibes.charges.create({++ amount: config.price,++ description: config.item,++ reference: ref,++ return_url: `${config.publicUrl}/thanks?order=${encodeURIComponent(ref)}`,++ });++ if (!charge || !charge.id || !charge.pay_url) throw new Error('Billing answered no charge id or pay_url');++ order.charge_id = charge.id;++ order.pay_url = charge.pay_url;++ order.status = charge.status || 'pending';++ byCharge.set(charge.id, order);++ return order;++ } catch (err) {++ orders.delete(ref);++ throw err;++ }++ }++++ /** Apply a verified webhook payload. The same delivery may arrive twice; the order changes once. */++ function applyEvent(event) {++ const type = String((event && event.type) || '');++ const data = event && event.data && typeof event.data === 'object' ? event.data : (event || {});++ const charge = data.charge && typeof data.charge === 'object' ? data.charge : data;++ const chargeId = String(charge.id || data.charge_id || data.id || '');++ const ref = String(charge.reference || data.reference || '');++ const status = String(charge.status || data.status || (type === 'billing.vibes.charge.paid' ? 'paid'++ : type === 'billing.vibes.charge.refunded' ? 'refunded' : ''));++ const order = (ref && orders.get(ref)) || (chargeId && byCharge.get(chargeId));++ if (!order) return { ok: false, reason: 'unknown_order', charge_id: chargeId || null, reference: ref || null };++ const was = order.status;++ if (status === 'paid') {++ if (order.status !== 'paid') { order.status = 'paid'; order.paid_at = new Date(now()).toISOString(); }++ } else if (status === 'refunded') {++ order.status = 'refunded';++ } else if ((status === 'cancelled' || status === 'expired') && order.status === 'pending') {++ order.status = status;++ }++ return { ok: true, reference: order.reference, charge_id: order.charge_id, status: order.status, duplicate: was === 'paid' && order.status === 'paid' };++ }++++ const server = http.createServer(async (req, res) => {++ const url = new URL(req.url, config.publicUrl);++ try {++ if (req.method === 'GET' && url.pathname === '/') {++ const order = await createOrder();++ const html = render(INDEX, { item: config.item, price: config.price, pay_url: order.pay_url, reference: order.reference });++ return send(res, 200, html, 'text/html; charset=utf-8');++ }++ if (req.method === 'GET' && url.pathname === '/thanks') {++ return send(res, 200, THANKS, 'text/html; charset=utf-8');++ }++ if (req.method === 'GET' && url.pathname === '/shop.css') {++ return send(res, 200, CSS, 'text/css; charset=utf-8');++ }++ if (req.method === 'GET' && url.pathname === '/vibes-pay.js') {++ return send(res, 200, VIBES_PAY, 'text/javascript; charset=utf-8');++ }++ if (req.method === 'POST' && url.pathname === '/api/orders') {++ const order = await createOrder();++ return send(res, 201, { order });++ }++ if (req.method === 'GET' && url.pathname.startsWith('/api/orders/')) {++ const ref = decodeURIComponent(url.pathname.slice('/api/orders/'.length));++ const order = orders.get(ref);++ if (!order) return send(res, 404, { ok: false, error: 'unknown_order' });++ return send(res, 200, { reference: order.reference, charge_id: order.charge_id, status: order.status, amount: order.amount, paid_at: order.paid_at });++ }++ if (req.method === 'POST' && url.pathname === '/webhooks/vibes') {++ const raw = await readBody(req);++ const signature = req.headers['x-openvibe-signature-v2'] || req.headers['x-openvibe-signature'] || '';++ if (!vibes.webhooks.verify(raw, signature, config.webhookSecret)) {++ return send(res, 401, { ok: false, error: 'bad_signature' });++ }++ let event;++ try { event = JSON.parse(raw.toString('utf8')); } catch { return send(res, 400, { ok: false, error: 'bad_json' }); }++ const out = applyEvent(event);++ // 2xx ends the retries; an unknown order is not an error (another deploy may own it).++ return send(res, 200, out);++ }++ if (req.method === 'GET' && url.pathname === '/health') return send(res, 200, { ok: true });++ return send(res, 404, { ok: false, error: 'not_found' });++ } catch (err) {++ logger.error && logger.error('[vibes-pay]', err && err.message ? err.message : err);++ return send(res, err && err.status === 413 ? 413 : 502, { ok: false, error: err && err.status === 413 ? 'body_too_large' : 'shop_unavailable' });++ }++ });++ server.orders = orders;++ server.createOrder = createOrder;++ server.applyEvent = applyEvent;++ return server;++}++++/** The production wiring: the real SDK, the real Billing. */++async function main(env = process.env) {++ const config = loadConfig(env);++ const { createVibesClient } = require('openvibe-sdk/vibes');++ const vibes = createVibesClient({ appToken: config.appToken, billingUrl: config.billingUrl });++ const server = createShop({ config, vibes });++ await new Promise((resolve, reject) => {++ server.once('error', reject);++ server.listen(config.port, '127.0.0.1', resolve);++ });++ console.log(`Vibes Pay shop: ${config.item} for ${config.price} Vibes at ${config.publicUrl}`);++ console.log('Webhook endpoint: POST /webhooks/vibes (X-OpenVibe-Signature-V2)');++ return { server, config };++}++++if (require.main === module) {++ main().catch((err) => { console.error(`vibes-pay: ${err.message}`); process.exitCode = 1; });++}++++module.exports = { loadConfig, createShop, main };nnew file mode 100644@@@ -0,0 +1,121 @@++'use strict';++/**++ * Smoke test: the shop against a local Billing stand-in, never the network. It uses the released++ * openvibe-sdk/vibes client for the charge and the webhook check, and the pinned++ * openvibe-shared v4.111.0 vibes-pay.js served by the shop. Only Billing is a stand-in.++ */++const assert = require('node:assert/strict');++const crypto = require('node:crypto');++const http = require('node:http');++const { createVibesClient } = require('openvibe-sdk/vibes');++const { loadConfig, createShop } = require('../server');++++const hmac = (secret, text) => crypto.createHmac('sha256', String(secret)).update(text).digest('hex');++const sign = (body, secret) => { const t = Math.floor(Date.now() / 1000); return `t=${t},v2=${hmac(secret, `${t}.${body}`)}`; };++++function readBody(req) {++ return new Promise((resolve) => { const c = []; req.on('data', (x) => c.push(x)); req.on('end', () => resolve(Buffer.concat(c))); });++}++const json = (res, status, body) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); };++++(async () => {++ assert.throws(() => loadConfig({}), /missing environment variables: OV_APP_TOKEN, OV_VIBES_WEBHOOK_SECRET/);++ assert.throws(() => loadConfig({ OV_APP_TOKEN: 'a', OV_VIBES_WEBHOOK_SECRET: 's', OV_PRICE_VIBES: '1.5' }), /positive whole number/);++++ const billingCalls = [];++ const charges = new Map();++ const billing = http.createServer(async (req, res) => {++ const body = await readBody(req);++ if (req.method === 'POST' && req.url === '/api/v1/vibes/charges') {++ const input = JSON.parse(body.toString('utf8'));++ billingCalls.push({ url: req.url, authorization: req.headers.authorization, idempotency: req.headers['idempotency-key'], body: input });++ if (req.headers.authorization !== 'Bearer app-token') return json(res, 401, { code: 'token.missing' });++ if (!Number.isSafeInteger(input.amount) || input.amount <= 0) return json(res, 422, { code: 'billing.invalid_input' });++ const id = `vch_${String(charges.size + 1).padStart(26, '0')}`;++ charges.set(input.reference, { id, pay_url: `https://openvibe.network/pay/${id}`, status: 'pending' });++ return json(res, 201, charges.get(input.reference));++ }++ return json(res, 404, { code: 'not_found' });++ });++ await new Promise((r) => billing.listen(0, '127.0.0.1', r));++ const billingUrl = `http://127.0.0.1:${billing.address().port}`;++++ // The released client, pointed at the local Billing stand-in.++ const vibes = createVibesClient({ appToken: 'app-token', billingUrl });++++ const config = loadConfig({++ OV_APP_TOKEN: 'app-token',++ OV_VIBES_WEBHOOK_SECRET: 'whsec_test_not_real',++ OV_BILLING_URL: billingUrl,++ OV_PUBLIC_URL: 'http://127.0.0.1',++ OV_ITEM_NAME: 'Sticker pack: Neon cats',++ OV_PRICE_VIBES: '50',++ });++ const shop = createShop({ config, vibes });++ await new Promise((r) => shop.listen(0, '127.0.0.1', r));++ const base = `http://127.0.0.1:${shop.address().port}`;++ config.publicUrl = base;++++ try {++ // 1. The page: it loads vibes-pay.js and carries the auto-bound button with the charge's pay_url.++ const pageRes = await fetch(`${base}/`);++ assert.equal(pageRes.status, 200);++ const page = await pageRes.text();++ assert.match(page, /<script src="\/vibes-pay\.js" defer><\/script>/);++ const shared = await (await fetch(`${base}/vibes-pay.js`)).text();++ assert.match(shared, /OpenVibe\.vibes/);++ assert.match(shared, /ov-vibes-pay/);++ const payUrl = page.match(/<button[^>]*data-ov-vibes-pay="([^"]+)"/)[1];++ assert.match(payUrl, /^https:\/\/openvibe\.network\/pay\/vch_/);++ const reference = page.match(/<main data-order="([^"]+)"/)[1];++ assert.match(reference, /^order_/);++++ // 2. The server created the charge with the SDK: the app token, whole Vibes, the order id, its return_url.++ assert.equal(billingCalls.length, 1);++ const call = billingCalls[0];++ assert.equal(call.authorization, 'Bearer app-token');++ assert.equal(call.idempotency, reference);++ assert.deepEqual(call.body, {++ amount: 50, description: 'Sticker pack: Neon cats', reference,++ return_url: `${base}/thanks?order=${reference}`,++ });++++ const order = async (ref = reference) => (await (await fetch(`${base}/api/orders/${encodeURIComponent(ref)}`)).json());++ assert.equal((await order()).status, 'pending');++++ // 3. A bad signature is refused and changes nothing.++ const event = JSON.stringify({ type: 'billing.vibes.charge.paid', data: { id: charges.get(reference).id, status: 'paid', reference } });++ const bad = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': 't=1,v2=' + '0'.repeat(64) }, body: event });++ assert.equal(bad.status, 401);++ assert.equal((await order()).status, 'pending');++++ // 4. The signed delivery marks the order paid; a replay says duplicate and changes nothing.++ const paid = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': sign(event, 'whsec_test_not_real') }, body: event });++ assert.equal(paid.status, 200);++ assert.deepEqual(await paid.json(), { ok: true, reference, charge_id: charges.get(reference).id, status: 'paid', duplicate: false });++ assert.equal((await order()).status, 'paid');++ const replay = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': sign(event, 'whsec_test_not_real') }, body: event });++ assert.equal(replay.status, 200);++ assert.equal((await replay.json()).duplicate, true);++ assert.equal((await order()).status, 'paid');++++ // 5. A refund is its own signed event; the return page is served.++ const refunded = JSON.stringify({ type: 'billing.vibes.charge.refunded', data: { id: charges.get(reference).id, status: 'refunded', reference } });++ const refund = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': sign(refunded, 'whsec_test_not_real') }, body: refunded });++ assert.equal(refund.status, 200);++ assert.equal((await order()).status, 'refunded');++ const thanks = await (await fetch(`${base}/thanks?order=${reference}`)).text();++ assert.match(thanks, /Back to the shop/);++++ // 6. Another order is a different reference and a different charge; the shop creates no duplicate.++ const second = await (await fetch(`${base}/api/orders`, { method: 'POST' })).json();++ assert.notEqual(second.order.reference, reference);++ assert.equal(billingCalls.length, 2);++ assert.equal(billingCalls[1].idempotency, second.order.reference);++++ console.log('vibes-pay example: ok');++ } finally {++ await new Promise((r) => shop.close(r));++ await new Promise((r) => billing.close(r));++ }++})().catch((err) => { console.error(err); process.exit(1); });@@@ -13,7 +13,7 @@"node": ">=22"},"dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0","@electric-sql/pglite": "^0.5.8","pg": "^8.23.0"}@@@ -15,7 +15,7 @@"create-openvibe-app": "scripts/create-openvibe-app.js"},"devDependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -27,47 +27,19 @@"license": "MIT","dependencies": {"@anthropic-ai/sdk": "1.4.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"}},-- "examples/ai-anthropic/node_modules/openvibe-sdk": {-- "version": "0.83.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.83.0",-- "integrity": "sha512-xHFcsZmRVuQX0HlMFA7Kc53GMpdZUXxkcR17whknFgUQznuWCwcnhwS87HnnqsrmnHp8AHC0d5uywLjfOwOYBw==",-- "license": "MIT",-- "bin": {-- "openvibe-download": "bin/openvibe-download.js"-- },-- "engines": {-- "node": ">=20"-- },-- "peerDependencies": {-- "@electric-sql/pglite": "^0.5.8",-- "iovalkey": "^0.4.0",-- "pg": "^8.23.0"-- },-- "peerDependenciesMeta": {-- "@electric-sql/pglite": {-- "optional": true-- },-- "iovalkey": {-- "optional": true-- },-- "pg": {-- "optional": true-- }-- }-- },"examples/ai-chat": {"name": "openvibe-example-ai-chat","version": "0.1.0","license": "MIT","dependencies": {"openai": "7.42.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -86,7 +58,7 @@"version": "0.2.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -108,7 +80,7 @@"version": "0.2.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -119,7 +91,7 @@"version": "0.1.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -130,7 +102,7 @@"version": "0.1.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -141,7 +113,7 @@"version": "0.1.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -152,7 +124,7 @@"version": "0.2.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -164,7 +136,7 @@"license": "MIT","dependencies": {"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -175,7 +147,7 @@"version": "0.2.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -186,7 +158,7 @@"version": "0.2.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -197,7 +169,7 @@"version": "0.1.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"},"engines": {"node": ">=22"@@@ -208,7 +180,19 @@"version": "0.2.0","license": "MIT","dependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"++ },++ "engines": {++ "node": ">=22"++ }++ },++ "examples/vibes-pay": {++ "name": "openvibe-example-vibes-pay",++ "version": "0.1.0",++ "license": "MIT",++ "dependencies": {++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.111.0"},"engines": {"node": ">=22"@@@ -220,7 +204,7 @@"license": "MIT","dependencies": {"@electric-sql/pglite": "^0.5.8",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0","pg": "^8.23.0"},"engines": {@@@ -730,12 +714,161 @@}}},++ "node_modules/boolbase": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",++ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",++ "license": "ISC",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/bowser": {"version": "2.14.1","resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz","integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==","license": "MIT"},++ "node_modules/buffer-equal-constant-time": {++ "version": "1.0.1",++ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",++ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",++ "license": "BSD-3-Clause"++ },++ "node_modules/css-select": {++ "version": "7.0.0",++ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",++ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0",++ "css-what": "^8.0.0",++ "domhandler": "^6.0.1",++ "domutils": "^4.0.2",++ "nth-check": "^3.0.1"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },++ "node_modules/css-what": {++ "version": "8.0.0",++ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",++ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },++ "node_modules/cssomnom": {++ "version": "0.1.11",++ "resolved": "https://registry.npmjs.org/cssomnom/-/cssomnom-0.1.11.tgz",++ "integrity": "sha512-P4RkO1Sq2Ual77xypEmAc4MkM5k2Ha4UwtBPCOpJmXculGCYPgzsU/xnOCy084esZaPePgGkkJg4Il/HE7GS5A==",++ "license": "Apache-2.0",++ "engines": {++ "node": ">=20.0.0"++ }++ },++ "node_modules/dom-serializer": {++ "version": "3.1.1",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",++ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0",++ "entities": "^8.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/domelementtype": {++ "version": "3.0.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",++ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ }++ },++ "node_modules/domhandler": {++ "version": "6.0.1",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",++ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^3.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/domutils": {++ "version": "4.0.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",++ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^3.0.0",++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },++ "node_modules/ecdsa-sig-formatter": {++ "version": "1.0.11",++ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",++ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",++ "license": "Apache-2.0",++ "dependencies": {++ "safe-buffer": "^5.0.1"++ }++ },++ "node_modules/entities": {++ "version": "8.1.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",++ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/fast-deep-equal": {"version": "3.1.3","resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",@@@ -764,6 +897,110 @@],"license": "BSD-3-Clause"},++ "node_modules/html-escaper": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",++ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",++ "license": "MIT"++ },++ "node_modules/htmlparser2": {++ "version": "10.1.0",++ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",++ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",++ "funding": [++ "https://github.com/fb55/htmlparser2?sponsor=1",++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3",++ "domutils": "^3.2.2",++ "entities": "^7.0.1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",++ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.2",++ "entities": "^4.2.0"++ },++ "funding": {++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {++ "version": "4.5.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domelementtype": {++ "version": "2.3.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",++ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause"++ },++ "node_modules/htmlparser2/node_modules/domhandler": {++ "version": "5.0.3",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",++ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^2.3.0"++ },++ "engines": {++ "node": ">= 4"++ },++ "funding": {++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domutils": {++ "version": "3.2.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",++ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^2.0.0",++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3"++ },++ "funding": {++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/entities": {++ "version": "7.0.1",++ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",++ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/json-schema-to-ts": {"version": "3.1.1","resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",@@@ -783,6 +1020,137 @@"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==","license": "MIT"},++ "node_modules/jsonwebtoken": {++ "version": "9.0.3",++ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",++ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",++ "license": "MIT",++ "dependencies": {++ "jws": "^4.0.1",++ "lodash.includes": "^4.3.0",++ "lodash.isboolean": "^3.0.3",++ "lodash.isinteger": "^4.0.4",++ "lodash.isnumber": "^3.0.3",++ "lodash.isplainobject": "^4.0.6",++ "lodash.isstring": "^4.0.1",++ "lodash.once": "^4.0.0",++ "ms": "^2.1.1",++ "semver": "^7.5.4"++ },++ "engines": {++ "node": ">=12",++ "npm": ">=6"++ }++ },++ "node_modules/jwa": {++ "version": "2.0.1",++ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",++ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",++ "license": "MIT",++ "dependencies": {++ "buffer-equal-constant-time": "^1.0.1",++ "ecdsa-sig-formatter": "1.0.11",++ "safe-buffer": "^5.0.1"++ }++ },++ "node_modules/jws": {++ "version": "4.0.1",++ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",++ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",++ "license": "MIT",++ "dependencies": {++ "jwa": "^2.0.1",++ "safe-buffer": "^5.0.1"++ }++ },++ "node_modules/linkedom": {++ "version": "0.18.15",++ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.15.tgz",++ "integrity": "sha512-o0w7eD/SvoCao+pUi6dzocCLuMR11g4Tv/et0W2OUzwWmUcewntSzyl+Ua/K+0yKym0Y/Mav3nFAaI5Av5jvZg==",++ "license": "ISC",++ "dependencies": {++ "css-select": "^7.0.0",++ "cssomnom": "^0.1.11",++ "html-escaper": "^3.0.3",++ "htmlparser2": "^10.1.0",++ "uhyphen": "^0.2.0"++ },++ "engines": {++ "node": ">=16"++ },++ "peerDependencies": {++ "canvas": ">= 2"++ },++ "peerDependenciesMeta": {++ "canvas": {++ "optional": true++ }++ }++ },++ "node_modules/lodash.includes": {++ "version": "4.3.0",++ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",++ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",++ "license": "MIT"++ },++ "node_modules/lodash.isboolean": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",++ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",++ "license": "MIT"++ },++ "node_modules/lodash.isinteger": {++ "version": "4.0.4",++ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",++ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",++ "license": "MIT"++ },++ "node_modules/lodash.isnumber": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",++ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",++ "license": "MIT"++ },++ "node_modules/lodash.isplainobject": {++ "version": "4.0.6",++ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",++ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",++ "license": "MIT"++ },++ "node_modules/lodash.isstring": {++ "version": "4.0.1",++ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",++ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",++ "license": "MIT"++ },++ "node_modules/lodash.once": {++ "version": "4.1.1",++ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",++ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",++ "license": "MIT"++ },++ "node_modules/ms": {++ "version": "2.1.3",++ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",++ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",++ "license": "MIT"++ },++ "node_modules/nth-check": {++ "version": "3.0.1",++ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",++ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/nth-check?sponsor=1"++ }++ },"node_modules/openai": {"version": "7.42.0","resolved": "https://registry.npmjs.org/openai/-/openai-7.42.0.tgz",@@@ -900,6 +1268,10 @@"resolved": "examples/tool-job","link": true},++ "node_modules/openvibe-example-vibes-pay": {++ "resolved": "examples/vibes-pay",++ "link": true++ },"node_modules/openvibe-example-webhook-consumer": {"resolved": "examples/webhook-consumer","link": true@@@ -909,9 +1281,9 @@"link": true},"node_modules/openvibe-sdk": {-- "version": "0.82.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "integrity": "sha512-oJarlzFGEkqYY0eFumIRjJUqMpOT70kgbhgeDtA8QHnuMZR3VIGQubM2UlOaqXgkxD5uhdZ/G6jLHbjvUhqTLQ==",++ "version": "0.84.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",++ "integrity": "sha512-OLngno3URAIUOqvEtQ9e/dvg3mhZrndGOEBEIgA7yn/uEDtYykgpc6HKajiyeub4UvZ8Dn2Vqn0WdqIYr/8/Uw==","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -936,6 +1308,19 @@}}},++ "node_modules/openvibe-shared": {++ "version": "4.111.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.111.0",++ "integrity": "sha512-cJZJnlJL4MT7gP7TtZIbsuEYI9S+941kwzQy2UiKSP/qazF0GqxneG/8VOv45PAV0eLcfgSVVkiXHuOVL+xirw==",++ "license": "MIT",++ "dependencies": {++ "jsonwebtoken": "^9.0.0",++ "linkedom": "^0.18.13"++ },++ "bin": {++ "openvibe-build-assets": "scripts/build-assets.js"++ }++ },"node_modules/pg": {"version": "8.23.1","resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",@@@ -1073,6 +1458,38 @@"node": ">=0.10.0"}},++ "node_modules/safe-buffer": {++ "version": "5.2.1",++ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",++ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/feross"++ },++ {++ "type": "patreon",++ "url": "https://www.patreon.com/feross"++ },++ {++ "type": "consulting",++ "url": "https://feross.org/support"++ }++ ],++ "license": "MIT"++ },++ "node_modules/semver": {++ "version": "7.8.6",++ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",++ "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",++ "license": "ISC",++ "bin": {++ "semver": "bin/semver.js"++ },++ "engines": {++ "node": ">=10"++ }++ },"node_modules/split2": {"version": "4.2.0","resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",@@@ -1104,6 +1521,12 @@"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==","license": "0BSD"},++ "node_modules/uhyphen": {++ "version": "0.2.0",++ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",++ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",++ "license": "ISC"++ },"node_modules/ws": {"version": "8.21.3","resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",@@@ -23,6 +23,6 @@"node": ">=22"},"devDependencies": {-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"}}@@@ -11,7 +11,7 @@ const fs = require('node:fs');const path = require('node:path');const ROOT = path.join(__dirname, '..');--const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];++const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'vibes-pay', 'webhook-consumer', 'zone-s3'];// The pin every example must share: the repository's own (the pin bot moves them all at once).const ROOT_PKG = require(path.join(ROOT, 'package.json'));const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];
@@ -3,13 +3,13 @@
@@ -3,13 +3,13 @@
> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials
> Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials
> and public APIs, nothing else.
> and public APIs, nothing else.
-**Status:** alpha, 0.3.0 (roadmap Wave 20; the `create-openvibe-app` scaffolder, WS-N task 6). Fifteen examples, each with a smoke test that CI runs on
+**Status:** alpha, 0.3.0 (roadmap Wave 20; the `create-openvibe-app` scaffolder, WS-N task 6). Eighteen examples, each with a smoke test that CI runs on
Node 22.22.1 against `openvibe-sdk/testing`'s mock platform (no network). `npm run e2e` runs three
Node 22.22.1 against `openvibe-sdk/testing`'s mock platform (no network). `npm run e2e` runs three
of them (Media, Events pull, a Tools job) against the real platform with your own sandbox app; it
of them (Media, Events pull, a Tools job) against the real platform with your own sandbox app; it
is not part of CI or `npm test`, and it was not run against production as part of this release.
is not part of CI or `npm test`, and it was not run against production as part of this release.
`npm run developer-path` is the Wave 20 exit check (account to Media, Events and AI to revoked
`npm run developer-path` is the Wave 20 exit check (account to Media, Events and AI to revoked
credentials); CI runs it against the mock platform, and OpenVibe.Host runs it against production daily.
credentials); CI runs it against the mock platform, and OpenVibe.Host runs it against production daily.
-**Built on:** [openvibe-sdk v0.82.0](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.62.0), and
+**Built on:** [openvibe-sdk v0.84.0](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.84.0), and
openvibe-contracts v0.222.0 for the mod manifest.
openvibe-contracts v0.222.0 for the mod manifest.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.1
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.1
and §18.9; roadmap Wave 20, §30 (public SDK surface), ADR-014 (developer projects).
and §18.9; roadmap Wave 20, §30 (public SDK surface), ADR-014 (developer projects).
@@ -42,6 +42,7 @@ Each example is small, runs on its own and proves one thing with a smoke test.
@@ -42,6 +42,7 @@ Each example is small, runs on its own and proves one thing with a smoke test.
| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
| [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |
| [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |
| [ai-anthropic](examples/ai-anthropic) | The official `@anthropic-ai/sdk` client on `https://ai.openvibe.services` (POST /v1/messages) with a Network app token through the SDK's `credentials`: the answer names the model that ran and carries `x_openvibe`. |
| [ai-anthropic](examples/ai-anthropic) | The official `@anthropic-ai/sdk` client on `https://ai.openvibe.services` (POST /v1/messages) with a Network app token through the SDK's `credentials`: the answer names the model that ran and carries `x_openvibe`. |
+| [vibes-pay](examples/vibes-pay) | Sell a sticker pack for Vibes: the server creates a charge with the SDK (the order id is the idempotency key), the page opens the pay sheet with `vibes-pay.js`, and a signed webhook marks the order paid once. | `openvibe-sdk/vibes` (`charges.create`, `webhooks.verify`), `openvibe-shared/vibes-pay.js` (`OpenVibe.vibes.pay`, `[data-ov-vibes-pay]`) |
| [ai-python](examples/ai-python) | The official `openai` Python client on `https://ai.openvibe.services/v1`, with a Network app token from client credentials kept and renewed a minute early. |
| [ai-python](examples/ai-python) | The official `openai` Python client on `https://ai.openvibe.services/v1`, with a Network app token from client credentials kept and renewed a minute early. |
Each example folder has a README, a `.env.example`, its own `package.json` and a
Each example folder has a README, a `.env.example`, its own `package.json` and a
@@ -132,6 +133,7 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.
@@ -132,6 +133,7 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.
| [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` |
| [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` |
| [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` |
| [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` |
| [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
| [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
+| [vibes-pay](examples/vibes-pay) | confidential | `billing.vibes.charge` | `OV_APP_TOKEN`, `OV_VIBES_WEBHOOK_SECRET`, `OV_BILLING_URL`, `OV_PUBLIC_URL`, `OV_ITEM_NAME`, `OV_PRICE_VIBES`, `OV_PORT` |
| [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
| [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with
Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with
@@ -367,5 +369,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -367,5 +369,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
-- openvibe-sdk: v0.82.0
+- openvibe-sdk: v0.84.0
<!-- versions:end -->
<!-- versions:end -->
@@ -5,11 +5,11 @@
@@ -5,11 +5,11 @@
"wave": "W20",
"wave": "W20",
"version": "0.3.0",
"version": "0.3.0",
"contracts": "openvibe-contracts v0.222.0 (mod-manifest example)",
"contracts": "openvibe-contracts v0.222.0 (mod-manifest example)",
- "sdk": "openvibe-sdk v0.82.0",
+ "sdk": "openvibe-sdk v0.84.0",
"deployed": false,
"deployed": false,
"deployment": "none: a repository with CI, not a runtime and not a published package",
"deployment": "none: a repository with CI, not a runtime and not a published package",
"features": [
"features": [
- "fifteen copyable examples: browser app, Node server app, Media uploader, event subscriber, webhook consumer, tool job, OAuth app, chat bot, mod manifest, AI chat (the official openai client on ai.openvibe.services/v1, plain, streamed and the project's own provider), go live (a Live API token sets up a WHIP slot; FFmpeg 8+ publishes; the session is named and ended), Zone over S3 (the official AWS SDK v3: put, get, paged list, presigned share link, delete), Space deploys from CI (an app token, live or preview, rollback, a GitHub Actions workflow), what people wear (Inventory's public catalog and equipped reads, no token), game scores (a sandbox- title on OpenVibe.Play: a round counted once, an unlock, the public board)",
+ "eighteen copyable examples: browser app, Node server app, Media uploader, event subscriber, webhook consumer, tool job, OAuth app, chat bot, mod manifest, AI chat (the official openai client on ai.openvibe.services/v1, plain, streamed and the project's own provider), go live (a Live API token sets up a WHIP slot; FFmpeg 8+ publishes; the session is named and ended), Zone over S3 (the official AWS SDK v3: put, get, paged list, presigned share link, delete), Space deploys from CI (an app token, live or preview, rollback, a GitHub Actions workflow), what people wear (Inventory's public catalog and equipped reads, no token), game scores (a sandbox- title on OpenVibe.Play: a round counted once, an unlock, the public board), Vibes Pay (a tiny sticker shop: a charge with openvibe-sdk/vibes, vibes-pay.js, and a signed webhook that marks the order paid)",
"smoke tests against openvibe-sdk/testing's mock platform, offline",
"smoke tests against openvibe-sdk/testing's mock platform, offline",
"create-openvibe-app scaffolder",
"create-openvibe-app scaffolder",
"npm run e2e against the real platform (on demand)",
"npm run e2e against the real platform (on demand)",
@@ -14,6 +14,6 @@
@@ -14,6 +14,6 @@
},
},
"dependencies": {
"dependencies": {
"@anthropic-ai/sdk": "1.4.0",
"@anthropic-ai/sdk": "1.4.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -14,6 +14,6 @@
@@ -14,6 +14,6 @@
},
},
"dependencies": {
"dependencies": {
"openai": "7.42.0",
"openai": "7.42.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0"
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -13,6 +13,6 @@
@@ -13,6 +13,6 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+# The app's token (capability billing.vibes.charge). Keep it on the server; never put it in the page.
+OV_APP_TOKEN=
+
+# The app's webhook secret from its page in OpenVibe.Services. Keep it on the server.
+OV_VIBES_WEBHOOK_SECRET=
+
+# Billing's origin and this shop's public origin (the return_url is built from it).
+OV_BILLING_URL=https://billing.openvibe.network
+OV_PUBLIC_URL=http://localhost:8099
+
+# The one item in this tiny shop.
+OV_PORT=8099
+OV_ITEM_NAME=Sticker pack: Neon cats
+OV_PRICE_VIBES=50
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
+# Vibes Pay: a tiny sticker shop
+
+Sell one thing for Vibes (MONEY.md, D12). The server creates the charge with `openvibe-sdk/vibes`; the page opens
+OpenVibe's pay sheet with `openvibe-shared/vibes-pay.js`; a signed webhook marks the order paid. No card form is drawn
+anywhere: the person pays Vibes they already have.
+
+```bash
+cp .env.example .env # fill OV_APP_TOKEN and OV_VIBES_WEBHOOK_SECRET
+npm start # http://localhost:8099
+```
+
+The app needs `billing.vibes.charge`; make a sandbox app in your project and grant it. Set its webhook URL to your
+public `/webhooks/vibes` endpoint.
+
+## What it proves
+
+- **The charge is the server's.** `GET /` calls `vibes.charges.create({ amount, description, reference,
+ return_url })` with the app token. `amount` is a whole number of Vibes, and the order id is the reference, so the
+ SDK sends it as the idempotency key: a retried create is the same charge, never a second one. The app token never
+ reaches the page.
+- **The page uses `vibes-pay.js`.** `server.js` serves the pinned `openvibe-shared` file at `/vibes-pay.js`, and
+ `public/index.html` has `<button data-ov-vibes-pay="{{pay_url}}">`; the button binds itself in the Vibes accent,
+ opens the sheet (a popup on a computer, a full page on a phone), and the page listens for the `ov-vibes-pay`
+ result as a hint to refresh.
+- **The webhook is the confirmation.** `POST /webhooks/vibes` reads the raw body, verifies
+ `X-OpenVibe-Signature-V2` with `vibes.webhooks.verify`, and marks the order paid once. A bad signature is 401 and
+ changes nothing; a repeat delivery is `duplicate: true`. A refund event moves the order to `refunded`.
+- **The page never decides money.** `GET /api/orders/:reference` is the server's own state; the return page
+ (`/thanks?order=…`) reads it too.
+
+The smoke test runs the shop against a local Billing stand-in, using the released `openvibe-sdk/vibes` client for the
+charge and the webhook check and the pinned `openvibe-shared` `vibes-pay.js`. It needs no network and no real Vibes.
+It checks the create body and idempotency key, the page's button and served `vibes-pay.js`, a refused signature, a
+paid delivery, its replay, a refund and a second order.
+
+## Run it against the real platform
+
+1. Create a project and a **sandbox app** on <https://openvibe.services>, then grant the app
+ `billing.vibes.charge`.
+2. Put the app's token and its webhook secret in `.env` (`OV_APP_TOKEN`, `OV_VIBES_WEBHOOK_SECRET`); set
+ `OV_PUBLIC_URL` to an HTTPS origin that reaches this server, because Billing posts the webhook there and the
+ `return_url` is built from it.
+3. `npm start`, open the shop, and pay with sandbox Vibes in the sheet.
+4. Open `/api/orders/<reference>` (or watch the server log): the webhook marks it `paid`. Refund it from the app's
+ server with `vibes.charges.refund(id)` and watch the refund event.
+
+Sandbox charges use sandbox Vibes (test grants worth nothing) and never touch a person's real Vibes. Production Vibes
+Pay opens when the owner enables Billing's `billing.vibes` settings; see openvibe.services `/docs/vibes` for the
+plain-HTTP flow, signatures, limits and payouts.
+
+## Pins
+
+`package.json` pins the two releases this example uses:
+
+- `openvibe-sdk`: **v0.84.0** (`openvibe-sdk/vibes`).
+- `openvibe-shared`: **v4.111.0** (`vibes-pay.js`, served by `server.js` at `/vibes-pay.js`).
+
+`npm install` fetches both from their release tags; no branch tarball is used.
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "name": "openvibe-example-vibes-pay",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Vibes Pay: a tiny sticker shop that creates a charge with openvibe-sdk/vibes, opens the pay sheet with vibes-pay.js, and marks the order paid from a signed webhook.",
+ "license": "MIT",
+ "main": "server.js",
+ "scripts": {
+ "start": "node --env-file-if-exists=.env server.js",
+ "test": "node test/smoke.test.js"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "dependencies": {
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.111.0"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="color-scheme" content="dark light">
+ <title>{{item}} — Vibes Pay shop</title>
+ <link rel="stylesheet" href="/shop.css">
+ <script src="/vibes-pay.js" defer></script>
+</head>
+<body>
+<main data-order="{{reference}}">
+ <p class="eyebrow">Vibes Pay shop</p>
+ <h1>{{item}}</h1>
+ <p class="lede">A tiny shop on OpenVibe. Pay once with Vibes; the webhook marks the order paid.</p>
+ <div class="sticker">
+ <div class="art" aria-hidden="true"></div>
+ <div><strong>{{item}}</strong><span class="price">{{price}} Vibes</span></div>
+ </div>
+ <div class="pay">
+ <button type="button" data-ov-vibes-pay="{{pay_url}}">Pay with Vibes</button>
+ <noscript><a href="{{pay_url}}">Pay with Vibes</a></noscript>
+ </div>
+ <p id="status" role="status" aria-live="polite"></p>
+ <p class="hint">You pay on OpenVibe's sheet with Vibes you already have. The sticker pack is delivered when the
+ signed webhook says the charge is paid. This page is an example; no real stickers are shipped.</p>
+</main>
+<script>
+ (function () {
+ var main = document.querySelector('main');
+ var order = main && main.getAttribute('data-order');
+ var status = document.getElementById('status');
+ if (!order || !status) return;
+
+ function show(text, kind) { status.textContent = text; status.className = kind || ''; }
+
+ async function check() {
+ try {
+ var res = await fetch('/api/orders/' + encodeURIComponent(order));
+ if (!res.ok) return;
+ var orderState = await res.json();
+ if (orderState.status === 'paid') show('Paid — the sticker pack is yours.', 'paid');
+ else if (orderState.status === 'refunded') show('Refunded to your Vibes.', '');
+ else show('Waiting for the payment…');
+ } catch (e) { show('Could not reach the shop. Try again.', 'error'); }
+ }
+
+ document.addEventListener('ov-vibes-pay', function (event) {
+ if (event.detail && event.detail.status === 'paid') {
+ show('Paid — the sticker pack is yours.', 'paid');
+ check();
+ } else if (event.detail && event.detail.status === 'closed') {
+ show('The sheet was closed. Nothing was charged.');
+ }
+ });
+ check();
+ }());
+</script>
+</body>
+</html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+/* The tiny shop's own look; the Pay button's look comes from openvibe.network/shared/vibes-pay.js. */
+:root { color-scheme: dark light; --bg: #0b0f17; --card: #121826; --text: #e8ecf3; --muted: #93a0b4; --line: rgba(255,255,255,.12); }
+@media (prefers-color-scheme: light) { :root { --bg: #f4f7fb; --card: #ffffff; --text: #111827; --muted: #5b6575; --line: rgba(17,24,39,.12); } }
+* { box-sizing: border-box; }
+body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: var(--bg); color: var(--text);
+ font: 16px/1.55 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; }
+main { width: min(560px, 100%); background: var(--card); border: 1px solid var(--line); border-radius: 20px; padding: clamp(22px, 5vw, 40px);
+ box-shadow: 0 24px 60px -36px rgba(0,0,0,.6); }
+.eyebrow { margin: 0 0 8px; color: #4ade80; font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
+h1 { margin: 0 0 8px; font-size: clamp(26px, 5vw, 34px); line-height: 1.15; }
+.lede { margin: 0 0 20px; color: var(--muted); }
+.sticker { display: flex; align-items: center; gap: 14px; padding: 16px; border: 1px solid var(--line); border-radius: 14px; margin: 0 0 20px; }
+.sticker .art { width: 64px; height: 64px; flex: none; border-radius: 12px; background: radial-gradient(circle at 35% 30%, #86efac, #22c55e 60%, #15803d); }
+.sticker strong { display: block; }
+.price { color: #4ade80; font-weight: 800; }
+.pay { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+.hint { color: var(--muted); font-size: 14px; margin: 14px 0 0; }
+#status { min-height: 1.5em; margin: 16px 0 0; font-weight: 600; }
+#status.paid { color: #16a34a; }
+#status.error { color: #dc2626; }
+a { color: inherit; }
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Thanks — Vibes Pay shop</title>
+ <link rel="stylesheet" href="/shop.css">
+</head>
+<body>
+<main>
+ <p class="eyebrow">Vibes Pay shop</p>
+ <h1 id="thanks-title">Thanks for the Vibes</h1>
+ <p class="lede" id="thanks-text">This is the return page. The shop will show the sticker pack as paid once the signed webhook confirms it.</p>
+ <p><a href="/">Back to the shop</a></p>
+</main>
+<script>
+ (function () {
+ var order = new URLSearchParams(location.search).get('order');
+ if (!order) return;
+ var title = document.getElementById('thanks-title');
+ var text = document.getElementById('thanks-text');
+ fetch('/api/orders/' + encodeURIComponent(order)).then(function (r) { return r.ok ? r.json() : null; }).then(function (o) {
+ if (!o) return;
+ if (o.status === 'paid') { title.textContent = 'Paid — the sticker pack is yours.'; text.textContent = 'The webhook confirmed order ' + o.reference + '.'; }
+ else if (o.status === 'refunded') { title.textContent = 'Refunded.'; text.textContent = 'The Vibes went back to your wallet.'; }
+ else { title.textContent = 'Waiting for the payment…'; }
+ }).catch(function () {});
+ }());
+</script>
+</body>
+</html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,231 @@
@@ -0,0 +1,231 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Vibes Pay: a tiny Node shop that sells one sticker pack for whole Vibes (MONEY.md, D12).
+ *
+ * 1. GET / creates an order and a charge with openvibe-sdk/vibes (the app token, capability
+ * billing.vibes.charge; the order id is the reference, so a retry is the same charge);
+ * 2. the page opens the pay sheet with openvibe-shared/vibes-pay.js (OpenVibe.vibes.pay, or the
+ * auto-bound [data-ov-vibes-pay] button);
+ * 3. Billing posts billing.vibes.charge.paid to /webhooks/vibes; the server verifies the raw body
+ * with the SDK and marks the order paid, once.
+ *
+ * The app token and the webhook secret stay here, never in the page. Run it with:
+ *
+ * npm start # reads .env (node --env-file-if-exists)
+ * node server.js
+ *
+ * This example only uses public surfaces: Billing's /api/v1/vibes routes and the SDK. The smoke test
+ * runs the same shop against a local Billing stand-in, never the network.
+ */
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const http = require('node:http');
+const path = require('node:path');
+
+const PUBLIC = path.join(__dirname, 'public');
+const INDEX = fs.readFileSync(path.join(PUBLIC, 'index.html'), 'utf8');
+const THANKS = fs.readFileSync(path.join(PUBLIC, 'thanks.html'), 'utf8');
+const CSS = fs.readFileSync(path.join(PUBLIC, 'shop.css'), 'utf8');
+const VIBES_PAY = fs.readFileSync(require.resolve('openvibe-shared/vibes-pay'), 'utf8');
+const MAX_BODY = 64 * 1024;
+
+function loadConfig(env = process.env) {
+ const missing = ['OV_APP_TOKEN', 'OV_VIBES_WEBHOOK_SECRET'].filter((k) => !env[k]);
+ if (missing.length) {
+ throw Object.assign(new Error(`missing environment variables: ${missing.join(', ')} (see .env.example)`), { code: 'config.missing' });
+ }
+ const port = Number(env.OV_PORT || 8099);
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('OV_PORT is a port number');
+ const price = Number(env.OV_PRICE_VIBES || 50);
+ if (!Number.isSafeInteger(price) || price <= 0) throw new Error('OV_PRICE_VIBES is a positive whole number of Vibes');
+ const publicUrl = String(env.OV_PUBLIC_URL || `http://localhost:${port}`).replace(/\/+$/, '');
+ let parsed;
+ try { parsed = new URL(publicUrl); } catch { throw new Error('OV_PUBLIC_URL is an http(s) URL'); }
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw new Error('OV_PUBLIC_URL is an http(s) URL');
+ return {
+ port,
+ publicUrl,
+ billingUrl: String(env.OV_BILLING_URL || 'https://billing.openvibe.network').replace(/\/+$/, ''),
+ appToken: env.OV_APP_TOKEN,
+ webhookSecret: env.OV_VIBES_WEBHOOK_SECRET,
+ item: env.OV_ITEM_NAME || 'Sticker pack: Neon cats',
+ price,
+ };
+}
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+}
+
+function render(template, vars) {
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (key in vars ? escapeHtml(vars[key]) : ''));
+}
+
+function readBody(req, limit = MAX_BODY) {
+ return new Promise((resolve, reject) => {
+ const chunks = [];
+ let size = 0;
+ req.on('data', (chunk) => {
+ size += chunk.length;
+ if (size > limit) {
+ const err = new Error('body too large');
+ err.status = 413;
+ reject(err);
+ req.destroy();
+ return;
+ }
+ chunks.push(chunk);
+ });
+ req.on('end', () => resolve(Buffer.concat(chunks)));
+ req.on('error', reject);
+ });
+}
+
+function send(res, status, body, type = 'application/json; charset=utf-8') {
+ const payload = typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body);
+ res.writeHead(status, { 'Content-Type': type, 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
+ res.end(payload);
+}
+
+/**
+ * Build the shop. `vibes` is the openvibe-sdk/vibes client; the smoke test passes the released client
+ * pointed at a local Billing stand-in. It must have charges.create/get/refund and webhooks.verify.
+ */
+function createShop({ config, vibes, logger = console, now = Date.now }) {
+ if (!config || !vibes) throw new TypeError('createShop: config and vibes are required');
+ if (!vibes.charges || typeof vibes.charges.create !== 'function') throw new TypeError('createShop: vibes.charges.create is required');
+ if (!vibes.webhooks || typeof vibes.webhooks.verify !== 'function') throw new TypeError('createShop: vibes.webhooks.verify is required');
+ const orders = new Map();
+ const byCharge = new Map();
+
+ function reference() {
+ return `order_${now().toString(36)}_${crypto.randomBytes(5).toString('hex')}`;
+ }
+
+ async function createOrder() {
+ const ref = reference();
+ const order = {
+ reference: ref,
+ charge_id: null,
+ pay_url: null,
+ amount: config.price,
+ item: config.item,
+ status: 'creating',
+ created_at: new Date(now()).toISOString(),
+ paid_at: null,
+ };
+ orders.set(ref, order);
+ try {
+ const charge = await vibes.charges.create({
+ amount: config.price,
+ description: config.item,
+ reference: ref,
+ return_url: `${config.publicUrl}/thanks?order=${encodeURIComponent(ref)}`,
+ });
+ if (!charge || !charge.id || !charge.pay_url) throw new Error('Billing answered no charge id or pay_url');
+ order.charge_id = charge.id;
+ order.pay_url = charge.pay_url;
+ order.status = charge.status || 'pending';
+ byCharge.set(charge.id, order);
+ return order;
+ } catch (err) {
+ orders.delete(ref);
+ throw err;
+ }
+ }
+
+ /** Apply a verified webhook payload. The same delivery may arrive twice; the order changes once. */
+ function applyEvent(event) {
+ const type = String((event && event.type) || '');
+ const data = event && event.data && typeof event.data === 'object' ? event.data : (event || {});
+ const charge = data.charge && typeof data.charge === 'object' ? data.charge : data;
+ const chargeId = String(charge.id || data.charge_id || data.id || '');
+ const ref = String(charge.reference || data.reference || '');
+ const status = String(charge.status || data.status || (type === 'billing.vibes.charge.paid' ? 'paid'
+ : type === 'billing.vibes.charge.refunded' ? 'refunded' : ''));
+ const order = (ref && orders.get(ref)) || (chargeId && byCharge.get(chargeId));
+ if (!order) return { ok: false, reason: 'unknown_order', charge_id: chargeId || null, reference: ref || null };
+ const was = order.status;
+ if (status === 'paid') {
+ if (order.status !== 'paid') { order.status = 'paid'; order.paid_at = new Date(now()).toISOString(); }
+ } else if (status === 'refunded') {
+ order.status = 'refunded';
+ } else if ((status === 'cancelled' || status === 'expired') && order.status === 'pending') {
+ order.status = status;
+ }
+ return { ok: true, reference: order.reference, charge_id: order.charge_id, status: order.status, duplicate: was === 'paid' && order.status === 'paid' };
+ }
+
+ const server = http.createServer(async (req, res) => {
+ const url = new URL(req.url, config.publicUrl);
+ try {
+ if (req.method === 'GET' && url.pathname === '/') {
+ const order = await createOrder();
+ const html = render(INDEX, { item: config.item, price: config.price, pay_url: order.pay_url, reference: order.reference });
+ return send(res, 200, html, 'text/html; charset=utf-8');
+ }
+ if (req.method === 'GET' && url.pathname === '/thanks') {
+ return send(res, 200, THANKS, 'text/html; charset=utf-8');
+ }
+ if (req.method === 'GET' && url.pathname === '/shop.css') {
+ return send(res, 200, CSS, 'text/css; charset=utf-8');
+ }
+ if (req.method === 'GET' && url.pathname === '/vibes-pay.js') {
+ return send(res, 200, VIBES_PAY, 'text/javascript; charset=utf-8');
+ }
+ if (req.method === 'POST' && url.pathname === '/api/orders') {
+ const order = await createOrder();
+ return send(res, 201, { order });
+ }
+ if (req.method === 'GET' && url.pathname.startsWith('/api/orders/')) {
+ const ref = decodeURIComponent(url.pathname.slice('/api/orders/'.length));
+ const order = orders.get(ref);
+ if (!order) return send(res, 404, { ok: false, error: 'unknown_order' });
+ return send(res, 200, { reference: order.reference, charge_id: order.charge_id, status: order.status, amount: order.amount, paid_at: order.paid_at });
+ }
+ if (req.method === 'POST' && url.pathname === '/webhooks/vibes') {
+ const raw = await readBody(req);
+ const signature = req.headers['x-openvibe-signature-v2'] || req.headers['x-openvibe-signature'] || '';
+ if (!vibes.webhooks.verify(raw, signature, config.webhookSecret)) {
+ return send(res, 401, { ok: false, error: 'bad_signature' });
+ }
+ let event;
+ try { event = JSON.parse(raw.toString('utf8')); } catch { return send(res, 400, { ok: false, error: 'bad_json' }); }
+ const out = applyEvent(event);
+ // 2xx ends the retries; an unknown order is not an error (another deploy may own it).
+ return send(res, 200, out);
+ }
+ if (req.method === 'GET' && url.pathname === '/health') return send(res, 200, { ok: true });
+ return send(res, 404, { ok: false, error: 'not_found' });
+ } catch (err) {
+ logger.error && logger.error('[vibes-pay]', err && err.message ? err.message : err);
+ return send(res, err && err.status === 413 ? 413 : 502, { ok: false, error: err && err.status === 413 ? 'body_too_large' : 'shop_unavailable' });
+ }
+ });
+ server.orders = orders;
+ server.createOrder = createOrder;
+ server.applyEvent = applyEvent;
+ return server;
+}
+
+/** The production wiring: the real SDK, the real Billing. */
+async function main(env = process.env) {
+ const config = loadConfig(env);
+ const { createVibesClient } = require('openvibe-sdk/vibes');
+ const vibes = createVibesClient({ appToken: config.appToken, billingUrl: config.billingUrl });
+ const server = createShop({ config, vibes });
+ await new Promise((resolve, reject) => {
+ server.once('error', reject);
+ server.listen(config.port, '127.0.0.1', resolve);
+ });
+ console.log(`Vibes Pay shop: ${config.item} for ${config.price} Vibes at ${config.publicUrl}`);
+ console.log('Webhook endpoint: POST /webhooks/vibes (X-OpenVibe-Signature-V2)');
+ return { server, config };
+}
+
+if (require.main === module) {
+ main().catch((err) => { console.error(`vibes-pay: ${err.message}`); process.exitCode = 1; });
+}
+
+module.exports = { loadConfig, createShop, main };
new file mode 100644
new file mode 100644
@@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
+'use strict';
+/**
+ * Smoke test: the shop against a local Billing stand-in, never the network. It uses the released
+ * openvibe-sdk/vibes client for the charge and the webhook check, and the pinned
+ * openvibe-shared v4.111.0 vibes-pay.js served by the shop. Only Billing is a stand-in.
+ */
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const http = require('node:http');
+const { createVibesClient } = require('openvibe-sdk/vibes');
+const { loadConfig, createShop } = require('../server');
+
+const hmac = (secret, text) => crypto.createHmac('sha256', String(secret)).update(text).digest('hex');
+const sign = (body, secret) => { const t = Math.floor(Date.now() / 1000); return `t=${t},v2=${hmac(secret, `${t}.${body}`)}`; };
+
+function readBody(req) {
+ return new Promise((resolve) => { const c = []; req.on('data', (x) => c.push(x)); req.on('end', () => resolve(Buffer.concat(c))); });
+}
+const json = (res, status, body) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); };
+
+(async () => {
+ assert.throws(() => loadConfig({}), /missing environment variables: OV_APP_TOKEN, OV_VIBES_WEBHOOK_SECRET/);
+ assert.throws(() => loadConfig({ OV_APP_TOKEN: 'a', OV_VIBES_WEBHOOK_SECRET: 's', OV_PRICE_VIBES: '1.5' }), /positive whole number/);
+
+ const billingCalls = [];
+ const charges = new Map();
+ const billing = http.createServer(async (req, res) => {
+ const body = await readBody(req);
+ if (req.method === 'POST' && req.url === '/api/v1/vibes/charges') {
+ const input = JSON.parse(body.toString('utf8'));
+ billingCalls.push({ url: req.url, authorization: req.headers.authorization, idempotency: req.headers['idempotency-key'], body: input });
+ if (req.headers.authorization !== 'Bearer app-token') return json(res, 401, { code: 'token.missing' });
+ if (!Number.isSafeInteger(input.amount) || input.amount <= 0) return json(res, 422, { code: 'billing.invalid_input' });
+ const id = `vch_${String(charges.size + 1).padStart(26, '0')}`;
+ charges.set(input.reference, { id, pay_url: `https://openvibe.network/pay/${id}`, status: 'pending' });
+ return json(res, 201, charges.get(input.reference));
+ }
+ return json(res, 404, { code: 'not_found' });
+ });
+ await new Promise((r) => billing.listen(0, '127.0.0.1', r));
+ const billingUrl = `http://127.0.0.1:${billing.address().port}`;
+
+ // The released client, pointed at the local Billing stand-in.
+ const vibes = createVibesClient({ appToken: 'app-token', billingUrl });
+
+ const config = loadConfig({
+ OV_APP_TOKEN: 'app-token',
+ OV_VIBES_WEBHOOK_SECRET: 'whsec_test_not_real',
+ OV_BILLING_URL: billingUrl,
+ OV_PUBLIC_URL: 'http://127.0.0.1',
+ OV_ITEM_NAME: 'Sticker pack: Neon cats',
+ OV_PRICE_VIBES: '50',
+ });
+ const shop = createShop({ config, vibes });
+ await new Promise((r) => shop.listen(0, '127.0.0.1', r));
+ const base = `http://127.0.0.1:${shop.address().port}`;
+ config.publicUrl = base;
+
+ try {
+ // 1. The page: it loads vibes-pay.js and carries the auto-bound button with the charge's pay_url.
+ const pageRes = await fetch(`${base}/`);
+ assert.equal(pageRes.status, 200);
+ const page = await pageRes.text();
+ assert.match(page, /<script src="\/vibes-pay\.js" defer><\/script>/);
+ const shared = await (await fetch(`${base}/vibes-pay.js`)).text();
+ assert.match(shared, /OpenVibe\.vibes/);
+ assert.match(shared, /ov-vibes-pay/);
+ const payUrl = page.match(/<button[^>]*data-ov-vibes-pay="([^"]+)"/)[1];
+ assert.match(payUrl, /^https:\/\/openvibe\.network\/pay\/vch_/);
+ const reference = page.match(/<main data-order="([^"]+)"/)[1];
+ assert.match(reference, /^order_/);
+
+ // 2. The server created the charge with the SDK: the app token, whole Vibes, the order id, its return_url.
+ assert.equal(billingCalls.length, 1);
+ const call = billingCalls[0];
+ assert.equal(call.authorization, 'Bearer app-token');
+ assert.equal(call.idempotency, reference);
+ assert.deepEqual(call.body, {
+ amount: 50, description: 'Sticker pack: Neon cats', reference,
+ return_url: `${base}/thanks?order=${reference}`,
+ });
+
+ const order = async (ref = reference) => (await (await fetch(`${base}/api/orders/${encodeURIComponent(ref)}`)).json());
+ assert.equal((await order()).status, 'pending');
+
+ // 3. A bad signature is refused and changes nothing.
+ const event = JSON.stringify({ type: 'billing.vibes.charge.paid', data: { id: charges.get(reference).id, status: 'paid', reference } });
+ const bad = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': 't=1,v2=' + '0'.repeat(64) }, body: event });
+ assert.equal(bad.status, 401);
+ assert.equal((await order()).status, 'pending');
+
+ // 4. The signed delivery marks the order paid; a replay says duplicate and changes nothing.
+ const paid = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': sign(event, 'whsec_test_not_real') }, body: event });
+ assert.equal(paid.status, 200);
+ assert.deepEqual(await paid.json(), { ok: true, reference, charge_id: charges.get(reference).id, status: 'paid', duplicate: false });
+ assert.equal((await order()).status, 'paid');
+ const replay = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': sign(event, 'whsec_test_not_real') }, body: event });
+ assert.equal(replay.status, 200);
+ assert.equal((await replay.json()).duplicate, true);
+ assert.equal((await order()).status, 'paid');
+
+ // 5. A refund is its own signed event; the return page is served.
+ const refunded = JSON.stringify({ type: 'billing.vibes.charge.refunded', data: { id: charges.get(reference).id, status: 'refunded', reference } });
+ const refund = await fetch(`${base}/webhooks/vibes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature-V2': sign(refunded, 'whsec_test_not_real') }, body: refunded });
+ assert.equal(refund.status, 200);
+ assert.equal((await order()).status, 'refunded');
+ const thanks = await (await fetch(`${base}/thanks?order=${reference}`)).text();
+ assert.match(thanks, /Back to the shop/);
+
+ // 6. Another order is a different reference and a different charge; the shop creates no duplicate.
+ const second = await (await fetch(`${base}/api/orders`, { method: 'POST' })).json();
+ assert.notEqual(second.order.reference, reference);
+ assert.equal(billingCalls.length, 2);
+ assert.equal(billingCalls[1].idempotency, second.order.reference);
+
+ console.log('vibes-pay example: ok');
+ } finally {
+ await new Promise((r) => shop.close(r));
+ await new Promise((r) => billing.close(r));
+ }
+})().catch((err) => { console.error(err); process.exit(1); });
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
"node": ">=22"
"node": ">=22"
},
},
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
"pg": "^8.23.0"
"pg": "^8.23.0"
}
}
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"create-openvibe-app": "scripts/create-openvibe-app.js"
"create-openvibe-app": "scripts/create-openvibe-app.js"
},
},
"devDependencies": {
"devDependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -27,47 +27,19 @@
@@ -27,47 +27,19 @@
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"@anthropic-ai/sdk": "1.4.0",
"@anthropic-ai/sdk": "1.4.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
}
}
},
},
- "examples/ai-anthropic/node_modules/openvibe-sdk": {
- "version": "0.83.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.83.0",
- "integrity": "sha512-xHFcsZmRVuQX0HlMFA7Kc53GMpdZUXxkcR17whknFgUQznuWCwcnhwS87HnnqsrmnHp8AHC0d5uywLjfOwOYBw==",
- "license": "MIT",
- "bin": {
- "openvibe-download": "bin/openvibe-download.js"
- },
- "engines": {
- "node": ">=20"
- },
- "peerDependencies": {
- "@electric-sql/pglite": "^0.5.8",
- "iovalkey": "^0.4.0",
- "pg": "^8.23.0"
- },
- "peerDependenciesMeta": {
- "@electric-sql/pglite": {
- "optional": true
- },
- "iovalkey": {
- "optional": true
- },
- "pg": {
- "optional": true
- }
- }
- },
"examples/ai-chat": {
"examples/ai-chat": {
"name": "openvibe-example-ai-chat",
"name": "openvibe-example-ai-chat",
"version": "0.1.0",
"version": "0.1.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"openai": "7.42.0",
"openai": "7.42.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -86,7 +58,7 @@
@@ -86,7 +58,7 @@
"version": "0.2.0",
"version": "0.2.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -108,7 +80,7 @@
@@ -108,7 +80,7 @@
"version": "0.2.0",
"version": "0.2.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -119,7 +91,7 @@
@@ -119,7 +91,7 @@
"version": "0.1.0",
"version": "0.1.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -130,7 +102,7 @@
@@ -130,7 +102,7 @@
"version": "0.1.0",
"version": "0.1.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -141,7 +113,7 @@
@@ -141,7 +113,7 @@
"version": "0.1.0",
"version": "0.1.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -152,7 +124,7 @@
@@ -152,7 +124,7 @@
"version": "0.2.0",
"version": "0.2.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -164,7 +136,7 @@
@@ -164,7 +136,7 @@
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -175,7 +147,7 @@
@@ -175,7 +147,7 @@
"version": "0.2.0",
"version": "0.2.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -186,7 +158,7 @@
@@ -186,7 +158,7 @@
"version": "0.2.0",
"version": "0.2.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -197,7 +169,7 @@
@@ -197,7 +169,7 @@
"version": "0.1.0",
"version": "0.1.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -208,7 +180,19 @@
@@ -208,7 +180,19 @@
"version": "0.2.0",
"version": "0.2.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "examples/vibes-pay": {
+ "name": "openvibe-example-vibes-pay",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.111.0"
},
},
"engines": {
"engines": {
"node": ">=22"
"node": ">=22"
@@ -220,7 +204,7 @@
@@ -220,7 +204,7 @@
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -730,12 +714,161 @@
@@ -730,12 +714,161 @@
}
}
}
}
},
},
+ "node_modules/boolbase": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
+ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/bowser": {
"node_modules/bowser": {
"version": "2.14.1",
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/css-select": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
+ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0",
+ "css-what": "^8.0.0",
+ "domhandler": "^6.0.1",
+ "domutils": "^4.0.2",
+ "nth-check": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
+ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssomnom": {
+ "version": "0.1.11",
+ "resolved": "https://registry.npmjs.org/cssomnom/-/cssomnom-0.1.11.tgz",
+ "integrity": "sha512-P4RkO1Sq2Ual77xypEmAc4MkM5k2Ha4UwtBPCOpJmXculGCYPgzsU/xnOCy084esZaPePgGkkJg4Il/HE7GS5A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
+ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0",
+ "entities": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
+ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
+ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
+ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^3.0.0",
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/fast-deep-equal": {
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -764,6 +897,110 @@
@@ -764,6 +897,110 @@
],
],
"license": "BSD-3-Clause"
"license": "BSD-3-Clause"
},
},
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmlparser2/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/json-schema-to-ts": {
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
@@ -783,6 +1020,137 @@
@@ -783,6 +1020,137 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/linkedom": {
+ "version": "0.18.15",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.15.tgz",
+ "integrity": "sha512-o0w7eD/SvoCao+pUi6dzocCLuMR11g4Tv/et0W2OUzwWmUcewntSzyl+Ua/K+0yKym0Y/Mav3nFAaI5Av5jvZg==",
+ "license": "ISC",
+ "dependencies": {
+ "css-select": "^7.0.0",
+ "cssomnom": "^0.1.11",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.1.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nth-check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
+ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/openai": {
"node_modules/openai": {
"version": "7.42.0",
"version": "7.42.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-7.42.0.tgz",
"resolved": "https://registry.npmjs.org/openai/-/openai-7.42.0.tgz",
@@ -900,6 +1268,10 @@
@@ -900,6 +1268,10 @@
"resolved": "examples/tool-job",
"resolved": "examples/tool-job",
"link": true
"link": true
},
},
+ "node_modules/openvibe-example-vibes-pay": {
+ "resolved": "examples/vibes-pay",
+ "link": true
+ },
"node_modules/openvibe-example-webhook-consumer": {
"node_modules/openvibe-example-webhook-consumer": {
"resolved": "examples/webhook-consumer",
"resolved": "examples/webhook-consumer",
"link": true
"link": true
@@ -909,9 +1281,9 @@
@@ -909,9 +1281,9 @@
"link": true
"link": true
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.82.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "integrity": "sha512-oJarlzFGEkqYY0eFumIRjJUqMpOT70kgbhgeDtA8QHnuMZR3VIGQubM2UlOaqXgkxD5uhdZ/G6jLHbjvUhqTLQ==",
+ "version": "0.84.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0",
+ "integrity": "sha512-OLngno3URAIUOqvEtQ9e/dvg3mhZrndGOEBEIgA7yn/uEDtYykgpc6HKajiyeub4UvZ8Dn2Vqn0WdqIYr/8/Uw==",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -936,6 +1308,19 @@
@@ -936,6 +1308,19 @@
}
}
}
}
},
},
+ "node_modules/openvibe-shared": {
+ "version": "4.111.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.111.0",
+ "integrity": "sha512-cJZJnlJL4MT7gP7TtZIbsuEYI9S+941kwzQy2UiKSP/qazF0GqxneG/8VOv45PAV0eLcfgSVVkiXHuOVL+xirw==",
+ "license": "MIT",
+ "dependencies": {
+ "jsonwebtoken": "^9.0.0",
+ "linkedom": "^0.18.13"
+ },
+ "bin": {
+ "openvibe-build-assets": "scripts/build-assets.js"
+ }
+ },
"node_modules/pg": {
"node_modules/pg": {
"version": "8.23.1",
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
@@ -1073,6 +1458,38 @@
@@ -1073,6 +1458,38 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
+ "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/split2": {
"node_modules/split2": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1104,6 +1521,12 @@
@@ -1104,6 +1521,12 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD"
},
},
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "license": "ISC"
+ },
"node_modules/ws": {
"node_modules/ws": {
"version": "8.21.3",
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
@@ -23,6 +23,6 @@
@@ -23,6 +23,6 @@
"node": ">=22"
"node": ">=22"
},
},
"devDependencies": {
"devDependencies": {
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.84.0"
}
}
}
}
@@ -11,7 +11,7 @@ const fs = require('node:fs');
@@ -11,7 +11,7 @@ const fs = require('node:fs');
const path = require('node:path');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
-const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
+const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'vibes-pay', 'webhook-consumer', 'zone-s3'];
// The pin every example must share: the repository's own (the pin bot moves them all at once).
// The pin every example must share: the repository's own (the pin bot moves them all at once).
const ROOT_PKG = require(path.join(ROOT, 'package.json'));
const ROOT_PKG = require(path.join(ROOT, 'package.json'));
const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];
const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];