openvibers/openvibe.examples Commits

examples/vibes-pay: a tiny shop that sells a sticker pack for Vibes (#56)

OP OpenVibers committed 8 days ago · 36c6d54d8e · 27 files, +1047 −67

* examples/vibes-pay: a tiny shop that sells a sticker pack for Vibes

The server creates the charge with openvibe-sdk/vibes (the order id is the Idempotency-Key), the
page opens the sheet with openvibe-shared/vibes-pay.js, and a signed webhook marks the order paid
once. The offline smoke test runs it against a Billing stand-in. package.json keeps the released
SDK pin; the README names the SDK 0.84.0 and Shared 4.111.0 pins still to bump.

* vibes-pay: pin the released SDK 0.84.0 and Shared 4.111.0

Pins openvibe-sdk v0.84.0 and openvibe-shared v4.111.0 in examples/vibes-pay,
serves the pinned vibes-pay.js from the package, and uses the released
createVibesClient in the smoke test. Moves the repository's shared SDK pin and
every example to v0.84.0, and removes the PR-branch tarball note.

Changes

ddiff --git a/README.md b/README.md
iindex 44bfbc4..5d6b966 100644
---- a/README.md
++++ b/README.md
@@@ -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 on
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
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
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.
**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).
@@@ -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 -->
ddiff --git a/STATUS.json b/STATUS.json
iindex ac3a663..93e6c92 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)",
ddiff --git a/examples/ai-anthropic/package.json b/examples/ai-anthropic/package.json
iindex 585984b..6bfcd2a 100644
---- a/examples/ai-anthropic/package.json
++++ b/examples/ai-anthropic/package.json
@@@ -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"
}
}
ddiff --git a/examples/ai-chat/package.json b/examples/ai-chat/package.json
iindex ea72525..53a7486 100644
---- a/examples/ai-chat/package.json
++++ b/examples/ai-chat/package.json
@@@ -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"
}
}
ddiff --git a/examples/browser-app/package.json b/examples/browser-app/package.json
iindex 00fc70a..33a198d 100644
---- a/examples/browser-app/package.json
++++ b/examples/browser-app/package.json
@@@ -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"
}
}
ddiff --git a/examples/event-subscriber/package.json b/examples/event-subscriber/package.json
iindex 6bb4a74..e622acb 100644
---- a/examples/event-subscriber/package.json
++++ b/examples/event-subscriber/package.json
@@@ -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"
}
}
ddiff --git a/examples/game-scores/package.json b/examples/game-scores/package.json
iindex b7245bb..f00627a 100644
---- a/examples/game-scores/package.json
++++ b/examples/game-scores/package.json
@@@ -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"
}
}
ddiff --git a/examples/go-live/package.json b/examples/go-live/package.json
iindex a62a087..8c6ab93 100644
---- a/examples/go-live/package.json
++++ b/examples/go-live/package.json
@@@ -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"
}
}
ddiff --git a/examples/inventory-wear/package.json b/examples/inventory-wear/package.json
iindex 56f0274..f10af2b 100644
---- a/examples/inventory-wear/package.json
++++ b/examples/inventory-wear/package.json
@@@ -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"
}
}
ddiff --git a/examples/media-uploader/package.json b/examples/media-uploader/package.json
iindex 415e1fe..b11995b 100644
---- a/examples/media-uploader/package.json
++++ b/examples/media-uploader/package.json
@@@ -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"
}
}
ddiff --git a/examples/mod-manifest/package.json b/examples/mod-manifest/package.json
iindex aa996fb..d12f881 100644
---- a/examples/mod-manifest/package.json
++++ b/examples/mod-manifest/package.json
@@@ -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"
}
}
ddiff --git a/examples/node-server-app/package.json b/examples/node-server-app/package.json
iindex c72d254..b6c8f36 100644
---- a/examples/node-server-app/package.json
++++ b/examples/node-server-app/package.json
@@@ -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"
}
}
ddiff --git a/examples/oauth-app/package.json b/examples/oauth-app/package.json
iindex fa5b1b3..6a35d1e 100644
---- a/examples/oauth-app/package.json
++++ b/examples/oauth-app/package.json
@@@ -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"
}
}
ddiff --git a/examples/space-deploy/package.json b/examples/space-deploy/package.json
iindex 7b9d6b3..b646eb5 100644
---- a/examples/space-deploy/package.json
++++ b/examples/space-deploy/package.json
@@@ -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"
}
}
ddiff --git a/examples/tool-job/package.json b/examples/tool-job/package.json
iindex 2f24a69..c305224 100644
---- a/examples/tool-job/package.json
++++ b/examples/tool-job/package.json
@@@ -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"
}
}
ddiff --git a/examples/vibes-pay/.env.example b/examples/vibes-pay/.env.example
nnew file mode 100644
iindex 0000000..d729d77
---- /dev/null
++++ b/examples/vibes-pay/.env.example
@@@ -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
ddiff --git a/examples/vibes-pay/README.md b/examples/vibes-pay/README.md
nnew file mode 100644
iindex 0000000..4445705
---- /dev/null
++++ b/examples/vibes-pay/README.md
@@@ -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.
ddiff --git a/examples/vibes-pay/package.json b/examples/vibes-pay/package.json
nnew file mode 100644
iindex 0000000..3224750
---- /dev/null
++++ b/examples/vibes-pay/package.json
@@@ -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"
++ }
++}
ddiff --git a/examples/vibes-pay/public/index.html b/examples/vibes-pay/public/index.html
nnew file mode 100644
iindex 0000000..ebfe209
---- /dev/null
++++ b/examples/vibes-pay/public/index.html
@@@ -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>
ddiff --git a/examples/vibes-pay/public/shop.css b/examples/vibes-pay/public/shop.css
nnew file mode 100644
iindex 0000000..3d62dbc
---- /dev/null
++++ b/examples/vibes-pay/public/shop.css
@@@ -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; }
ddiff --git a/examples/vibes-pay/public/thanks.html b/examples/vibes-pay/public/thanks.html
nnew file mode 100644
iindex 0000000..bdd6fc4
---- /dev/null
++++ b/examples/vibes-pay/public/thanks.html
@@@ -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>
ddiff --git a/examples/vibes-pay/server.js b/examples/vibes-pay/server.js
nnew file mode 100644
iindex 0000000..166306b
---- /dev/null
++++ b/examples/vibes-pay/server.js
@@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 };
ddiff --git a/examples/vibes-pay/test/smoke.test.js b/examples/vibes-pay/test/smoke.test.js
nnew file mode 100644
iindex 0000000..ebc4f32
---- /dev/null
++++ b/examples/vibes-pay/test/smoke.test.js
@@@ -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); });
ddiff --git a/examples/webhook-consumer/package.json b/examples/webhook-consumer/package.json
iindex d57b324..a8fb29d 100644
---- a/examples/webhook-consumer/package.json
++++ b/examples/webhook-consumer/package.json
@@@ -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"
}
ddiff --git a/package-lock.json b/package-lock.json
iindex fa8da7d..8070552 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex 16f413c..99bd0a5 100644
---- a/package.json
++++ b/package.json
@@@ -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"
}
}
ddiff --git a/test/structure.test.js b/test/structure.test.js
iindex c7754cf..5878fd7 100644
---- a/test/structure.test.js
++++ b/test/structure.test.js
@@@ -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'];