openvibers/openvibe.examples README.md
README.md
| 1 | # OpenVibe.Examples |
| 2 | |
| 3 | > Executable public integration examples for the OpenVibe platform: the SDK, scoped credentials |
| 4 | > and public APIs, nothing else. |
| 5 | |
| 6 | **Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt |
| 7 | **Install and test:** `npm ci && npm test` (Node 22 or newer) |
| 8 | |
| 9 | **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 |
| 10 | Node 22.22.1 against `openvibe-sdk/testing`'s mock platform (no network). `npm run e2e` runs three |
| 11 | of them (Media, Events pull, a Tools job) against the real platform with your own sandbox app; it |
| 12 | is not part of CI or `npm test`, and it was not run against production as part of this release. |
| 13 | `npm run developer-path` is the Wave 20 exit check (account to Media, Events and AI to revoked |
| 14 | credentials); CI runs it against the mock platform, and OpenVibe.Host runs it against production daily. |
| 15 | **Built on:** [openvibe-sdk v0.161.0](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.91.0), and |
| 16 | openvibe-contracts v0.419.0 for the mod manifest. |
| 17 | **Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.1 |
| 18 | and §18.9; roadmap Wave 20, §30 (public SDK surface), ADR-014 (developer projects). |
| 19 | **License:** MIT ([LICENSE](LICENSE)), so you can copy any example into your own project. The |
| 20 | OpenVibe services themselves are AGPL-3.0. |
| 21 | |
| 22 | ## Purpose |
| 23 | |
| 24 | Show, with code a developer can copy, how an app outside the network uses OpenVibe: only the public |
| 25 | SDK, scoped developer-app credentials and public APIs, never a loopback key or a first-party database. |
| 26 | Each example is small, runs on its own and proves one thing with a smoke test. |
| 27 | |
| 28 | ## The examples |
| 29 | |
| 30 | | Example | What it proves | Uses | |
| 31 | |---|---|---| |
| 32 | | [browser-app](examples/browser-app) | A plain page (no build step) imports the SDK's browser bundle, reads the registry cross-origin and signs in with PKCE; a public app needs no secret | `openvibe-sdk/browser/openvibe-sdk.mjs`, `/auth` (`exchangeCode`, `verifyAppToken`) | |
| 33 | | [node-server-app](examples/node-server-app) | Client-credentials app token per audience and what it carries, registry discovery, contracts version check, trace propagation | `openvibe-sdk/auth` (`getTokenInfo`), `/registry`, `/core` | |
| 34 | | [media-uploader](examples/media-uploader) | Upload into, read from and delete in your project's Media tenant; sandbox files come back with signed URLs | `openvibe-sdk/media` | |
| 35 | | [event-subscriber](examples/event-subscriber) | Publish to your project's topic `app.<project_key>.*`, pull it with a durable cursor, gaps; anonymous realtime for public events | `openvibe-sdk/events` (`createAppEvents`), `/realtime` | |
| 36 | | [webhook-consumer](examples/webhook-consumer) | An app subscription on your own topic; `X-OpenVibe-Signature-V2` (±300 s replay window) verified on the raw body; exactly-once handling with an inbox; secret rotation | `openvibe-sdk/events` (`createAppEvents`, `parseDelivery`, `createPgInbox`) | |
| 37 | | [tool-job](examples/tool-job) | An OpenVibe.Tools job submitted once (idempotency key), reattached after a restart, its SSE stream resumed with `Last-Event-ID` | `openvibe-sdk/jobs` | |
| 38 | | [oauth-app](examples/oauth-app) | Authorization code + PKCE for a confidential app, exchange on the server, offline app-token verification, session hygiene | `openvibe-sdk/auth` | |
| 39 | | [chat-bot](examples/chat-bot) | A bot on Chat's WebSocket protocol that stays in its one configured room and always identifies as a bot | Chat `/ws/chat` protocol (no SDK client: Chat is first-party) | |
| 40 | | [mod-manifest](examples/mod-manifest) | A `mods.mod-manifest@1` validated against the schema, the capability catalog and compatibility ranges | `openvibe-contracts`, `openvibe-sdk/registry` | |
| 41 | | [go-live](examples/go-live) | Go live on OpenVibe.Live from code: a Live API token finds or creates a WHIP slot and reads its WHIP URL, FFmpeg 8+ publishes (test pattern, a file or a camera), the session is named once media flows and ended when FFmpeg stops; the stream key is never printed | `openvibe-sdk/live` (`slots`, `streams`, `channels.url`), `/core`, FFmpeg's WHIP muxer | |
| 42 | | [game-scores](examples/game-scores) | Achievements and a leaderboard for your game on OpenVibe.Play, from a sandbox app any developer can make today: register a `sandbox-` title, post a round (counted once on a retry), unlock, read the public board, delete the title | `openvibe-sdk/play` (`titles`, `scores`, `achievements`, `boards`) | |
| 43 | | [inventory-wear](examples/inventory-wear) | Show what people wear on OpenVibe (hats, chat tags, badges, name effects) next to names in your own app: the public catalog and who wears what, 100 people per call, with no token; text and escaped HTML | `openvibe-sdk/inventory` (`kinds`, `definitions`, `equipped`) | |
| 44 | | [space-deploy](examples/space-deploy) | Deploy a static site to OpenVibe.Space from CI with an app token (space.deploy.create, deployer role): the built folder as one deploy, live on main or a preview for a pull request, a refusal that changes nothing, rollback; a ready GitHub Actions workflow | `openvibe-sdk/space` (`deploys.uploadFiles`, `rollback`), `/auth` | |
| 45 | | [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` | |
| 46 | | [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` | |
| 47 | | [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`. | |
| 48 | | [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]`) | |
| 49 | | [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. | |
| 50 | |
| 51 | Each example folder has a README, a `.env.example`, its own `package.json` and a |
| 52 | `test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`, |
| 53 | `OV_CLIENT_SECRET`, `OV_PROJECT_ID`, …); secrets are never logged or sent to a browser. |
| 54 | |
| 55 | ## Start a new app |
| 56 | |
| 57 | `create-openvibe-app` copies one of these examples into a new folder as your starting point, with |
| 58 | its smoke test, a `.gitignore`, the package renamed and the SDK pinned to the same tag. It needs no |
| 59 | network beyond fetching this package. |
| 60 | |
| 61 | ```bash |
| 62 | npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web |
| 63 | cd my-app && npm install && cp .env.example .env && npm test |
| 64 | ``` |
| 65 | |
| 66 | | Template | Starts from | For | |
| 67 | |---|---|---| |
| 68 | | `web` | [browser-app](examples/browser-app) | a plain web page that signs in with OpenVibe (PKCE) and calls a public API | |
| 69 | | `server` | [node-server-app](examples/node-server-app) | a Node server with a client-credentials app token and registry discovery | |
| 70 | | `bot` | [chat-bot](examples/chat-bot) | a chat bot that stays in its one room and always identifies as a bot | |
| 71 | | `mod` | [mod-manifest](examples/mod-manifest) | a mod manifest validated against the schema, capabilities and compatibility ranges | |
| 72 | |
| 73 | Without `--template` it asks on a terminal (and picks `web` otherwise). `npm create openvibe-app` |
| 74 | will work the same way once the package is on the npm registry; until then use the `npx` line. |
| 75 | |
| 76 | ## End-to-end walkthrough |
| 77 | |
| 78 | From a new account to the examples running against the platform, with public surfaces only. |
| 79 | Nothing here needs staff: a new project is `sandbox`, and a sandbox app's owner gets the sandbox |
| 80 | capabilities approved at once. |
| 81 | |
| 82 | **1. Create an account** at <https://openvibe.network>. |
| 83 | |
| 84 | **2. Create a project, a sandbox app and its grants.** Either: |
| 85 | |
| 86 | - **in the developer console, <https://openvibe.services>:** sign in, create a project, add a |
| 87 | **confidential** app in the `sandbox` environment with the redirect URI |
| 88 | `http://localhost:3009/callback` (for [oauth-app](examples/oauth-app)), copy the client secret |
| 89 | (it is shown **once**), and request the grants `media.object.upload`, `media.object.read`, |
| 90 | `events.app.publish`, `events.app.read`, `events.app.subscribe`, `tools.job.create`, |
| 91 | `tools.job.read`; or |
| 92 | - **with the projects API** (`openvibe-sdk/projects`): `npm run new-app` does exactly the above |
| 93 | and writes `OV_CLIENT_ID`, `OV_CLIENT_SECRET` and `OV_PROJECT_ID` into `./.env` (mode 0600) |
| 94 | without printing the secret. It signs in with your username and password (not echoed) or uses |
| 95 | `OV_USER_TOKEN`. The same in code: |
| 96 | |
| 97 | ```js |
| 98 | const { createClient } = require('openvibe-sdk/core'); |
| 99 | const { createProjectsClient } = require('openvibe-sdk/projects'); |
| 100 | const projects = createProjectsClient(createClient({ token: userAccessToken })); // a Network user token |
| 101 | const project = await projects.create({ name: 'My first OpenVibe app' }); // sandbox, you are the owner |
| 102 | const app = await projects.apps.create(project.id, { name: 'server', environment: 'sandbox', type: 'confidential' }); |
| 103 | app.credential.client_secret; // shown ONCE: store it now |
| 104 | await projects.grants.request(project.id, app.id, 'media.object.upload'); // approved at once for the owner |
| 105 | ``` |
| 106 | |
| 107 | For [browser-app](examples/browser-app) add a second, **public** app with the redirect URI |
| 108 | `http://localhost:3001/callback` and `media.object.read` (`npm run new-app -- --public --project |
| 109 | prj_…`). A public app has no secret. |
| 110 | |
| 111 | **3. Check it works end to end:** |
| 112 | |
| 113 | ```bash |
| 114 | npm ci |
| 115 | npm run e2e # reads ./.env: OV_CLIENT_ID, OV_CLIENT_SECRET, OV_PROJECT_ID |
| 116 | ``` |
| 117 | |
| 118 | It uploads a small file to your Media tenant, reads it back and deletes it; publishes |
| 119 | `app.<project_key>.e2e.ran` and pulls your topic until it finds it; converts a 1x1 PNG to WebP on |
| 120 | `openvibe.tools` (the gateway's jobs facade), following the job's events. It prints each step (never the secret, a token |
| 121 | or a URL signature) and exits `1` if a step failed, `2` if a variable is missing. |
| 122 | |
| 123 | **4. Run the examples.** Each reads `.env` in its own folder (`cp .env.example .env`), or point it |
| 124 | at the root one: `node --env-file=../../.env upload.js ./logo.png`. |
| 125 | |
| 126 | | Example | App | Grants | Environment variables | |
| 127 | |---|---|---|---| |
| 128 | | [node-server-app](examples/node-server-app) | confidential | any | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, `OV_AUDIENCES` | |
| 129 | | [media-uploader](examples/media-uploader) | confidential | `media.object.upload`, `media.object.read` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_PROJECT_ID` | |
| 130 | | [event-subscriber](examples/event-subscriber) | confidential | `events.app.publish`, `events.app.read` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_PROJECT_ID`, `OV_TOPICS`, `OV_PLATFORM_TOPICS`; realtime: `OV_EVENTS_MODE=realtime`, `OV_TOPICS` only | |
| 131 | | [webhook-consumer](examples/webhook-consumer) | confidential | `events.app.subscribe` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`; the consumer: `OV_WEBHOOK_SECRET` (written by `subscribe.js`); a public https host | |
| 132 | | [tool-job](examples/tool-job) | confidential | `tools.job.create`, `tools.job.read` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_TOOLS_JOBS_URL`, `OV_JOB_TYPE`, `OV_JOB_INPUT` | |
| 133 | | [oauth-app](examples/oauth-app) | confidential, redirect `http://localhost:3009/callback` | every capability in `OV_SCOPE` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, `OV_AUDIENCE`, `OV_SCOPE` | |
| 134 | | [browser-app](examples/browser-app) | public, redirect `http://localhost:3001/callback` | `media.object.read` | `OV_CLIENT_ID`, `OV_AUDIENCE`, `OV_SCOPE` | |
| 135 | | [chat-bot](examples/chat-bot) | none: a Live API token (`hbt_…`) of a dedicated bot account | Live token scope `chat` | `OV_CHAT_TOKEN`, one of `OV_CHAT_CHANNEL_USER_ID` / `OV_CHAT_STREAM_ID` | |
| 136 | | [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` | |
| 137 | | [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` | |
| 138 | | [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` | |
| 139 | | [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` | |
| 140 | | [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` | |
| 141 | |
| 142 | Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with |
| 143 | `projects.apps.revoke()`. Issued tokens end within their 5-minute lifetime. |
| 144 | |
| 145 | ## How the platform works for a sandbox app |
| 146 | |
| 147 | Verified in production on 2026-09-23 with a brand-new account, using the public endpoints only: |
| 148 | |
| 149 | - **Projects.** A new project is `sandbox`. Its owner's grant requests for the sandbox allowance |
| 150 | are approved at once: `media.object.upload|read`, `events.app.publish|read|subscribe`, |
| 151 | `tools.job.create|read|cancel`. |
| 152 | - **Tokens.** `POST https://openvibe.network/oauth/token` with `grant_type=client_credentials` |
| 153 | gives a confidential sandbox app a 5-minute token for `openvibe.media`, `openvibe.events` or |
| 154 | `openvibe.tools`, with `project_id`, `env: sandbox`, `ns: [project_id]` and the approved |
| 155 | capabilities for that audience. |
| 156 | - **Media.** The tenant is the project id: `/api/v1/<prj_…>/files`. Sandbox objects are stored in |
| 157 | a separate sandbox tenant (`<prj_…>-sandbox`, not addressable by path) and never served |
| 158 | publicly: signed URLs only. |
| 159 | - **Events.** App events are `app.<project_key>.<name…>` (`project_key` = `p` + the lowercased |
| 160 | project ULID) with `source: app-<lowercased app ULID>`. Read them with |
| 161 | `GET /api/v1/events?topic=app.<project_key>.*`; subscribe with public https endpoints only. |
| 162 | Realtime (SSE) never streams app events. |
| 163 | - **Tools.** `/api/v1/jobs` accepts sandbox app tokens. |
| 164 | - **Discovery.** `/.well-known/openvibe` and `/api/v1/registry/*` answer any origin (CORS), so a |
| 165 | browser page reads them directly. |
| 166 | |
| 167 | ## Developer path check |
| 168 | |
| 169 | `scripts/developer-path.js` is the roadmap Wave 20 exit check as a repeatable script: a developer |
| 170 | goes from an account to a working Media, Events and AI integration with public endpoints, the SDK and |
| 171 | scoped credentials only, then rotates and revokes those credentials and cleans up. OpenVibe.Host runs it |
| 172 | against production every day (`openvibe-devpath.timer`, shown on openvibe.network/status). |
| 173 | |
| 174 | | Step | What it does | |
| 175 | |---|---| |
| 176 | | 1. account | registers `OV_E2E_USERNAME` (`--register`) or signs in at `/api/auth/*`, or uses `OV_USER_TOKEN` | |
| 177 | | 2. discovery | reads `/.well-known/openvibe` as any origin may | |
| 178 | | 3. project | creates a sandbox project with `openvibe-sdk/projects` | |
| 179 | | 4. app | creates a confidential sandbox app; its secret stays in memory | |
| 180 | | 5. grants | requests `media.object.upload`, `media.object.read`, `events.app.publish`, `events.app.read`, `ai.app.run`, `play.title.manage`, `play.result.write`, `services.release.manage`; all must be approved at once | |
| 181 | | 6. media | [media-uploader](examples/media-uploader): upload a small file, read it back, delete it | |
| 182 | | 7. events | [event-subscriber](examples/event-subscriber): publish `app.<project_key>.developer_path.ran`, pull it with a cursor | |
| 183 | | 8. ai | [ai-chat](examples/ai-chat): the official `openai` client on OpenVibe.AI's OpenAI-compatible endpoint (`OV_AI_URL`) with an app token for `openvibe.ai`; one short answer, which must carry its `x_openvibe` run id | |
| 184 | | 9. play | [game-scores](examples/game-scores): a `sandbox-` title on OpenVibe.Play (`OV_PLAY_URL`), a round posted twice and recorded once, an unlock, the public board read without a token, the title deleted | |
| 185 | | 10. export | as the project's owner: Network mints 5-minute read-only export tokens; Media lists the project's sandbox files with one and refuses an upload; Events returns the event from step 7 with the other | |
| 186 | | 11. release | as the app, on OpenVibe.Services (`OV_SERVICES_URL`): a sandbox release of itself goes draft → published → deprecated → revoked, checked in the public list each time | |
| 187 | | 12. credentials | rotates with no overlap and revokes the first credential; the old secret must be refused at `/oauth/token`, the new one must work | |
| 188 | | 13. cleanup | archives the project, even when an earlier step failed; the app's secret must then be refused | |
| 189 | |
| 190 | **In CI**, on every push, `npm run developer-path:mock` runs the same code against |
| 191 | `openvibe-sdk/testing`'s mock platform (the `developer-path` job; `test/developer-path.test.js` |
| 192 | also checks it leaves nothing behind, never prints a secret, and still cleans up after a failure). |
| 193 | The mock has no account API, so `scripts/developer-path-mock.js` adds register and login routes |
| 194 | that mint mock user tokens, mints export tokens, and stands in for Services' release API, AI's |
| 195 | `/v1/chat/completions` and Play's title, score, unlock and board routes (with its sandbox rules); it hands the mock the contract's public capabilities, as Network reads them. Every |
| 196 | other step is the platform mock's own. |
| 197 | |
| 198 | **Against production** it runs only when a person starts it. Credentials come from the |
| 199 | environment (or `./.env`) only: |
| 200 | |
| 201 | ```bash |
| 202 | OV_E2E_USERNAME=… OV_E2E_PASSWORD=… npm run developer-path # an existing account |
| 203 | OV_E2E_USERNAME=… OV_E2E_PASSWORD=… npm run developer-path -- --register # create that account first |
| 204 | OV_USER_TOKEN=… npm run developer-path # a Network user access token |
| 205 | ``` |
| 206 | |
| 207 | It creates one project per run and archives it at the end (Network keeps archived projects). |
| 208 | It refuses to start without credentials, and refuses in CI against the production Network (`CI` |
| 209 | set and `OV_NETWORK_URL` unset or `https://openvibe.network`), so it can later run in CI against |
| 210 | an integration environment but never against production by accident. It masks every password, |
| 211 | secret and token in its output and prints signed URLs without their signature. Exit `0` when all |
| 212 | thirteen steps passed, `1` when one failed, `2` when it refused to start. |
| 213 | |
| 214 | It does not yet cover webhook delivery to an external endpoint (that needs a public https host) |
| 215 | or publishing an app release in OpenVibe.Services. |
| 216 | |
| 217 | ### Tools job proof |
| 218 | |
| 219 | `scripts/tools-job-proof.js` (`npm run tools-job-proof`) is the scheduled end-to-end proof of |
| 220 | OpenVibe.Tools jobs (roadmap WS-L task 4), again with the SDK only. A **service principal** runs it, |
| 221 | because sandbox jobs are never announced to Events or copied to Media: |
| 222 | |
| 223 | 1. **token**: client credentials for `tools.job.create` and `tools.job.read` (`openvibe.tools`) and |
| 224 | `events.event.read` (`openvibe.events`). |
| 225 | 2. **cursor**: the head of the Events store for `tools.job.*`, before anything is submitted. |
| 226 | 3. **submit**: `img.process` converts a small generated PNG to WebP. |
| 227 | 4. **reattach**: the progress stream is dropped after its first event and reattached with |
| 228 | `Last-Event-ID`, as the Tools UI does after a reload. Every later event must arrive once, in |
| 229 | order, up to `job.succeeded`. |
| 230 | 5. **result**: the job reads back `succeeded`, and its file is stored in Media (`storage: "media"` |
| 231 | with a media id). The download must be a WebP that matches the listed sha256. Tools keeps no |
| 232 | local copy of a Media-stored result. |
| 233 | 6. **events**: `tools.job.created`, `tools.job.started` and `tools.job.succeeded` for the job are |
| 234 | in the Events store. |
| 235 | |
| 236 | ```bash |
| 237 | OV_CLIENT_ID=… OV_CLIENT_SECRET=… npm run tools-job-proof [-- --result last.json] |
| 238 | ``` |
| 239 | |
| 240 | `OV_OAUTH_CLIENT_ID` and `OV_OAUTH_CLIENT_SECRET` also work; Network's service-principal setup |
| 241 | writes those names. OpenVibe.Host runs it every six hours as the `probe` principal |
| 242 | (`openvibe-toolsjob.timer`). CI runs it against the mock platform (`test/tools-job-proof.test.js`), |
| 243 | and it refuses to run in CI against production. |
| 244 | |
| 245 | ## What still does not work |
| 246 | |
| 247 | - **No refresh tokens, and no profile.** Network's consent screen lists the capabilities an app asks for, and a |
| 248 | person who continues gives it a 5-minute app token with no refresh token. The app learns only the person's `usr_…` |
| 249 | id (`on_behalf_of`): profile lookups (`identity.subject.resolve`) are `first-party`. |
| 250 | - **`/oauth/token` is not CORS-open**, like every Network route outside discovery and the |
| 251 | registry, so a browser app exchanges its code on its own server |
| 252 | ([browser-app](examples/browser-app) does). |
| 253 | - **Chat has no app principal.** `chat.message.send` is `first-party`; the |
| 254 | [chat-bot](examples/chat-bot) signs in with a Live API token (`hbt_…`, scope `chat`) of a |
| 255 | dedicated bot account, not with a developer app. |
| 256 | - **Production apps need staff**: switching a project to `sandbox+production` and setting its |
| 257 | allowance. |
| 258 | - **Publishing a mod** into Games is not public (`games.mod.manage` is `first-party`). |
| 259 | |
| 260 | ## Running the tests |
| 261 | |
| 262 | ```bash |
| 263 | fnm use 22.22.1 # or any Node 22 |
| 264 | npm ci # one install for all examples (npm workspaces) |
| 265 | npm test # every smoke test + the repository checks |
| 266 | npm test -- chat oauth # only some |
| 267 | cd examples/tool-job && npm test |
| 268 | ``` |
| 269 | |
| 270 | How the tests stay offline: |
| 271 | |
| 272 | - Network, Events, Media and Tools jobs are `createMockPlatform()` from `openvibe-sdk/testing`: an |
| 273 | in-process fake at the real public origins, with real RS256 tokens, developer apps and projects, |
| 274 | sandbox handling per service, `/oauth/authorize` with auto-consent, retention gaps |
| 275 | (`pruneEvents()`), a delivery worker (`deliverEvents()`) and Tools jobs with SSE. |
| 276 | - The mock plays the developer-app rules the way production does: Events' `events.app.*` |
| 277 | (own-project topics, `app-<ULID>` sources, env separation), Media's project tenants (`prj_…` and |
| 278 | `prj_…-sandbox`, signed URLs for sandbox files) and Tools at its satellites' origins. |
| 279 | - Chat is the only service the SDK does not mock: chat-bot carries `test/mock-chat-server.js`. |
| 280 | - Every smoke test replaces the global `fetch` with one that throws, so an accidental real network |
| 281 | call fails the test. |
| 282 | |
| 283 | Repository checks (`test/`): |
| 284 | |
| 285 | - `public-surface.test.js` fails on the loopback shared-key header, on the platform's internal |
| 286 | route prefix, on hard-coded loopback service ports outside the local mocks, and on any secret |
| 287 | value in a `.env.example`. |
| 288 | - `structure.test.js` checks the ten examples are complete and copyable: README with "What it |
| 289 | proves" and "Run it against the real platform", a `.env.example` that names every variable the |
| 290 | code reads, MIT license, `openvibe-sdk` pinned to one tag (the root package.json's), no `file:` links; and that |
| 291 | `npm run e2e` is outside CI and `npm test` and refuses to start without its variables; and that |
| 292 | CI runs the developer path against the mock platform only. |
| 293 | - `developer-path.test.js` runs the developer path against the mock platform and checks the |
| 294 | result (see [Developer path check](#developer-path-check)). |
| 295 | |
| 296 | ## Layout |
| 297 | |
| 298 | ``` |
| 299 | examples/<name>/ one example: README.md, .env.example, package.json, code, test/smoke.test.js |
| 300 | scripts/e2e.js npm run e2e: three examples against the real platform (on demand only) |
| 301 | scripts/developer-path.js npm run developer-path: the Wave 20 exit check against the real platform (on demand only) |
| 302 | scripts/developer-path-mock.js npm run developer-path:mock: the same flow against the mock platform (CI) |
| 303 | scripts/new-sandbox-app.js npm run new-app: project + sandbox app + grants via openvibe-sdk/projects |
| 304 | test/run.js runs every smoke test and the repository checks (npm test) |
| 305 | test/public-surface.test.js, test/structure.test.js |
| 306 | .github/workflows/ci.yml Node 22.22.1: npm test; the developer path against the mock platform |
| 307 | ``` |
| 308 | |
| 309 | ## Owns |
| 310 | |
| 311 | - the nine charter examples: vanilla browser app, Node server app, webhook consumer, event |
| 312 | subscriber, Media uploader, Chat bot, tool/job example, mod manifest example, OAuth app example; plus the AI chat example (the openai client on AI's OpenAI-compatible endpoint) the go-live example (Live over WHIP from code), the zone-s3 example (Zone with the AWS SDK v3), the space-deploy example (Space deploys from CI) and the inventory-wear example (what people wear, rendered in your app) |
| 313 | - the on-demand end-to-end run (`npm run e2e`) and the sandbox-app setup script |
| 314 | - the developer path check (`npm run developer-path`, and `developer-path:mock` in CI) |
| 315 | |
| 316 | ## Does not own |
| 317 | |
| 318 | - any platform service, the SDK (OpenVibe.SDK), the contracts (OpenVibe.Contracts) or the |
| 319 | developer console (OpenVibe.Services) |
| 320 | |
| 321 | ## Depends on |
| 322 | |
| 323 | - OpenVibe.SDK (v0.65.0), OpenVibe.Contracts (v0.214.0, mod manifest only) |
| 324 | - at run time against the real platform: Network (tokens, registry, developer projects), Events, |
| 325 | Media, Tools, Chat (on openvibe.live) |
| 326 | |
| 327 | ## Capabilities |
| 328 | |
| 329 | Examples implements no capability (its manifest lists none). The examples call the platform as a |
| 330 | developer app with the grants a sandbox project asks for (`npm run new-app`): `media.object.upload` |
| 331 | and `media.object.read` (Media), `events.app.publish`, `events.app.read` and `events.app.subscribe` |
| 332 | (Events), and `tools.job.create`, `tools.job.read` and `tools.job.cancel` (Tools); a public browser app |
| 333 | gets `media.object.read` only. The developer path check also uses `ai.app.run` (AI) and `services.release.manage` (Services). The |
| 334 | chat bot uses a Live `hbt_` token with scope `chat`, because `chat.message.send` is first-party. |
| 335 | |
| 336 | ## Acceptance |
| 337 | |
| 338 | - CI runs every example: **yes**, against the SDK's mock platform (and a small Chat WebSocket |
| 339 | mock for chat-bot; no network), and the developer path check against the same mock. Against |
| 340 | the real platform on demand: `npm run e2e` with a sandbox app's credentials and |
| 341 | `npm run developer-path` with an account; there is no integration environment with |
| 342 | credentials for CI. |
| 343 | - A new developer goes from account creation to a working Media, event and capability |
| 344 | integration using only public documentation, the SDK and scoped credentials: the platform path |
| 345 | was verified in production on 2026-09-23 (see |
| 346 | [How the platform works](#how-the-platform-works-for-a-sandbox-app)) by a curl script against the |
| 347 | public endpoints, not by the SDK or these examples. That check is now committed as |
| 348 | `npm run developer-path` (SDK and examples, plus credential rotation and revocation), runs in CI |
| 349 | against the mock platform, and has not yet been run against production. `npm run e2e` walks |
| 350 | three of the nine examples along the same path. **Partly met.** |
| 351 | - No example needs a loopback-only internal key or first-party database access: **yes**, enforced |
| 352 | by `test/public-surface.test.js`. |
| 353 | |
| 354 | ## Security |
| 355 | |
| 356 | Reporting a vulnerability: [SECURITY.md](SECURITY.md). Configuration is environment variables only; |
| 357 | secrets are never logged or sent to a browser, and `.env.example` files hold names, not values |
| 358 | (`test/public-surface.test.js` fails on a secret value there, on the loopback shared-key header and on |
| 359 | internal routes or ports). Every smoke test replaces `fetch` with one that throws, so no test reaches the |
| 360 | network. `npm run developer-path` refuses to run against production in CI. |
| 361 | |
| 362 | ## Deploy |
| 363 | |
| 364 | Nothing is deployed from this repository: it has no unit, port or env file and is not published as a |
| 365 | package. CI (`.github/workflows/ci.yml`) runs `npm test` and the developer path against the mock |
| 366 | platform on every push. Copy an example, or start one with `create-openvibe-app`, and deploy it as your |
| 367 | own app. |
| 368 | |
| 369 | --- |
| 370 | |
| 371 | Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers). |
| 372 | |
| 373 | <!-- versions:start --> |
| 374 | - openvibe-contracts: v0.419.0 |
| 375 | - openvibe-sdk: v0.161.0 |
| 376 | - openvibe-shared: v4.237.0 |
| 377 | <!-- versions:end --> |
| 378 |