openvibers/openvibe.sdk Commits
0.95.0: openvibe-sdk/cam loads the OpenVibe camera element (#130)
Changes
@@@ -1,5 +1,15 @@# Changelog++## 0.95.0 (2026-11-07)++++- Add `openvibe-sdk/cam`: `defineOvCamera({ src })` injects OpenVibe.Cam's framework-free `<ov-camera>` element++ once (default `https://openvibe.cam/embed/ov-camera.js`) and resolves to the class. It is browser-only by nature:++ calling it in Node rejects instead of touching `document`. `OV_CAMERA_SRC` is the default URL.++- Type the element: `OvCameraElement` (`start()`, `stop()`, `capture()`, `flip()`), `OvCameraAttributes` (facing,++ filters, timer, mode, upload), `OvCameraCaptureDetail` / `OvCameraErrorDetail`, the two event types and the++ `HTMLElementTagNameMap` entry. `upload="off"` is still the default: a capture is a `Blob` in the page until the++ site sends it somewhere.++## 0.94.0 (2026-11-07)- `openvibe-sdk/streams` gains S7: `schedule()`, `listSchedules()`, `cancelSchedule()` and `remindSchedule()` for@@@ -2,16 +2,16 @@> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.94.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.++**Status:** alpha, v0.95.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.--After v0.94.0 is tagged:++After v0.95.0 is tagged:```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.95.0```It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).@@@ -488,6 +488,31 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr`browser/openvibe-sdk.mjs` is generated by `node scripts/browser-bundle.js` from the CommonJS sources reachable from `browser.js` (no dependencies, no transpiling) and checked in. It exports core at the top level and `auth` (browser build), `registry`, `modules`, `realtime`, `media`, `community`, `bot`, `jobs`, `tools` and `projects` as namespaces. `test/bundle.test.js` fails when it is stale, and the browser secret scan covers it.++### The OpenVibe camera element++++`openvibe-sdk/cam` loads the camera app's framework-free `<ov-camera>` element from OpenVibe.Cam and types its++attributes, events and methods. It injects the embed script once and resolves to the element class:++++```js++import { defineOvCamera } from 'openvibe-sdk/cam';++++await defineOvCamera(); // https://openvibe.cam/embed/ov-camera.js, once++const camera = document.querySelector('ov-camera');++camera.addEventListener('ov-camera:capture', (event) => {++ // event.detail = { blob, width, height, filter }; nothing was uploaded++ upload(event.detail.blob);++});++await camera.start(); // or let the person click the element's own button++```++++```html++<ov-camera facing="user" filters="none,mono,warm" timer="3" mode="photo" upload="off"></ov-camera>++```++++`upload="off"` (the default) keeps the picture in the page. `upload="openvibe"` posts it to `POST /api/v1/photos` as++the signed-in person, and only on openvibe.cam. `defineOvCamera({ src })` loads a self-hosted copy of the same file.++The full attribute, event and method reference is at [openvibe.cam/developers](https://openvibe.cam/developers).++## Versioning- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.@@@ -3,12 +3,13 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.94.0",++ "version": "0.95.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag","contracts": "openvibe-contracts v0.312.0","features": [++ "cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default","streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end","reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps","chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.94.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.95.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, locker, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -48,7 +48,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.94.0';++const SDK_VERSION = '0.95.0';module.exports = {createClient,@@@ -479,6 +479,10 @@"openvibe-sdk/cache": {"createCache": "function"},++ "openvibe-sdk/cam": {++ "OV_CAMERA_SRC": "string",++ "defineOvCamera": "function"++ },"openvibe-sdk/chat": {"createChatClient": "function","createFeedPublisher": "function",nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/cam/index.js. Do not edit.++import cjs from '../src/cam/index.js';++++export const {++ defineOvCamera, OV_CAMERA_SRC,++} = cjs;++export default cjs;@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.94.0",++ "version": "0.95.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.94.0",++ "version": "0.95.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.94.0",++ "version": "0.95.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -347,6 +347,11 @@"import": "./esm/app-signin.mjs","default": "./src/app-signin/index.js"},++ "./cam": {++ "types": "./types/cam.d.ts",++ "import": "./esm/cam.mjs",++ "default": "./src/cam/index.js"++ },"./run": {"types": "./types/run.d.ts","browser": null,@@@ -28,6 +28,7 @@ const ENTRIES = {'events.mjs': '../src/events.js','sso.mjs': '../src/sso/index.js','app-signin.mjs': '../src/app-signin/index.js',++ 'cam.mjs': '../src/cam/index.js','realtime.mjs': '../src/realtime.js','media.mjs': '../src/media.js','community.mjs': '../src/community.js',nnew file mode 100644@@@ -0,0 +1,61 @@++'use strict';++/**++ * openvibe-sdk/cam: the OpenVibe camera element, from https://openvibe.cam/embed/ov-camera.js.++ *++ * In an app: import { defineOvCamera } from the package's cam entry.++ * await defineOvCamera();++ * const camera = document.querySelector('ov-camera');++ * camera.addEventListener('ov-camera:capture', (event) => upload(event.detail.blob));++ *++ * The embed itself is OpenVibe.Cam's framework-free custom element. This loader only injects one++ * <script> per origin (the page may already include it) and resolves to the class once the browser++ * has defined it. Nothing here touches the camera: the element starts only from a click or an++ * explicit start(), and its default upload="off" means a capture never leaves the page.++ *++ * <ov-camera facing="user" filters="none,mono,warm" timer="3" mode="photo" upload="off"></ov-camera>++ *++ * Browser-only by nature; in Node the call rejects and the module still loads (the types and the++ * pack test import it). Pass `src` to self-host the same element file.++ */++const OV_CAMERA_SRC = 'https://openvibe.cam/embed/ov-camera.js';++++// One promise per src: a second call while the script is in flight gets the same load, never a second tag.++const pending = new Map();++++/**++ * Inject the <ov-camera> embed once and resolve to the element class.++ * @param {{ src?: string }} [options]++ * @returns {Promise<CustomElementConstructor>}++ */++function defineOvCamera(options = {}) {++ const src = String((options && options.src) || OV_CAMERA_SRC);++ if (typeof window === 'undefined' || typeof document === 'undefined') {++ return Promise.reject(new Error('openvibe-sdk/cam: defineOvCamera() needs a browser with custom elements.'));++ }++ const defined = () => (window.customElements ? window.customElements.get('ov-camera') : undefined);++ const already = defined();++ if (already) return Promise.resolve(already);++ if (pending.has(src)) return pending.get(src);++++ const load = new Promise((resolve, reject) => {++ const script = document.createElement('script');++ script.src = src;++ script.async = true;++ script.dataset.ovCamera = '';++ script.addEventListener('load', () => {++ const ctor = defined();++ if (ctor) resolve(ctor);++ else reject(new Error(`openvibe-sdk/cam: ${src} loaded but did not define <ov-camera>.`));++ });++ script.addEventListener('error', () => {++ script.remove();++ reject(new Error(`openvibe-sdk/cam: could not load ${src}`));++ });++ (document.head || document.documentElement).appendChild(script);++ }).finally(() => pending.delete(src));++++ pending.set(src, load);++ return load;++}++++module.exports = { defineOvCamera, OV_CAMERA_SRC };@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.94.0';++const SDK_VERSION = '0.95.0';module.exports = {createClient,@@@ -78,7 +78,7 @@ run([assert.deepEqual(problems, []);assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './commerce', './consent', './cost', './coupons', './db', './deals', './delivery', './download', './events', './exchange', './food', './govern', './host', './identity', './inventory', './limits', './live', './news', './node', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windows-- for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {++ for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/cam/index.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {assert.ok(rel.includes(must), `${must} is scanned`);}for (const never of ['src/auth/tokens.js', 'src/auth/jwt.js', 'src/auth/oauth.js', 'src/events.js', 'src/identity.js', 'src/sso/index.js', 'src/app-signin/index.js', 'src/service/index.js', 'src/service-outbox.js', 'src/share.js', 'src/share/store.js', 'src/testing/index.js']) {nnew file mode 100644@@@ -0,0 +1,96 @@++'use strict';++/**++ * openvibe-sdk/cam: the export resolves by name (CJS and ESM), the loader injects the embed once in a fake DOM++ * and resolves to the element class, Node gets a rejection instead of a document crash, and the type file names++ * the runtime exports and the whole element surface.++ */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { run } = require('./helpers');++++const ROOT = path.join(__dirname, '..');++const cam = require('../src/cam');++const pkg = require('../package.json');++++run([++ ['the entry resolves by name and names the loader and the default script', async () => {++ assert.deepEqual(Object.keys(cam).sort(), ['OV_CAMERA_SRC', 'defineOvCamera']);++ assert.equal(cam.OV_CAMERA_SRC, 'https://openvibe.cam/embed/ov-camera.js');++ assert.equal(require('openvibe-sdk/cam'), cam);++ assert.deepEqual(pkg.exports['./cam'], {++ types: './types/cam.d.ts',++ import: './esm/cam.mjs',++ default: './src/cam/index.js',++ });++ const esm = await import('openvibe-sdk/cam');++ assert.equal(esm.default, cam);++ assert.equal(esm.OV_CAMERA_SRC, cam.OV_CAMERA_SRC);++ assert.equal(typeof esm.defineOvCamera, 'function');++ }],++++ ['in Node it rejects instead of touching a document', async () => {++ assert.equal(typeof global.window, 'undefined');++ await assert.rejects(cam.defineOvCamera(), /needs a browser/);++ await assert.rejects(cam.defineOvCamera({ src: 'https://example.test/ov-camera.js' }), /needs a browser/);++ }],++++ ['a browser gets one <script> per load and the class; a defined element resolves without another', async () => {++ const appended = [];++ const listeners = {};++ let ctor = null;++ const script = {++ dataset: {},++ addEventListener(type, fn) { listeners[type] = fn; },++ remove() { this.removed = true; },++ };++ const hadWindow = Object.prototype.hasOwnProperty.call(global, 'window');++ const hadDocument = Object.prototype.hasOwnProperty.call(global, 'document');++ const oldWindow = global.window;++ const oldDocument = global.document;++ global.window = { customElements: { get: (name) => (name === 'ov-camera' ? ctor : undefined) } };++ global.document = {++ createElement: () => script,++ head: {++ appendChild: (s) => {++ appended.push(s.src);++ ctor = function OvCamera() {};++ queueMicrotask(() => listeners.load());++ },++ },++ documentElement: { appendChild: () => { throw new Error('head exists; documentElement must not be used'); } },++ };++ try {++ const [a, b] = await Promise.all([cam.defineOvCamera(), cam.defineOvCamera()]);++ assert.equal(appended.length, 1, 'two concurrent calls share one script');++ assert.equal(a, ctor);++ assert.equal(b, ctor);++ assert.equal(script.dataset.ovCamera, '', 'the tag is marked as ours');++ assert.equal(script.async, true);++++ const again = await cam.defineOvCamera();++ assert.equal(again, ctor);++ assert.equal(appended.length, 1, 'a defined element needs no second script');++++ // A custom src is honoured when the page has not defined the element yet.++ ctor = null;++ const custom = await cam.defineOvCamera({ src: 'https://cdn.example/ov-camera.js' });++ assert.equal(typeof custom, 'function');++ assert.deepEqual(appended, ['https://openvibe.cam/embed/ov-camera.js', 'https://cdn.example/ov-camera.js']);++ } finally {++ if (hadWindow) global.window = oldWindow; else delete global.window;++ if (hadDocument) global.document = oldDocument; else delete global.document;++ }++ }],++++ ['the types file names the runtime exports and the element surface', async () => {++ const dts = fs.readFileSync(path.join(ROOT, 'types/cam.d.ts'), 'utf8');++ for (const name of [++ 'OV_CAMERA_SRC', 'defineOvCamera', 'DefineOvCameraOptions',++ 'OvCameraAttributes', 'OvCameraFacing', 'OvCameraFilter', 'OvCameraTimer', 'OvCameraMode', 'OvCameraUpload',++ 'OvCameraElement', 'OvCameraCaptureDetail', 'OvCameraErrorCode', 'OvCameraErrorDetail',++ 'OvCameraCaptureEvent', 'OvCameraErrorEvent', 'OvCameraEventMap', 'HTMLElementTagNameMap',++ 'start(): Promise<void>', 'stop(): void', 'capture(): Promise<', 'flip(): Promise<OvCameraFacing>',++ ]) assert.ok(dts.includes(name), name);++ }],++]);@@@ -24,7 +24,7 @@ run([const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8', shell });const info = JSON.parse(out)[0];const files = info.files.map((f) => f.path);-- for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/vibes.js', 'esm/vibes.mjs', 'types/vibes.d.ts', 'src/exchange.js', 'esm/exchange.mjs', 'types/exchange.d.ts', 'docs/exchange.md', 'src/bot.js', 'esm/bot.mjs', 'types/bot.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'src/service/index.js', 'esm/service.mjs', 'types/service.d.ts', 'docs/service.md', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'src/resources.js', 'esm/resources.mjs', 'types/resources.d.ts', 'src/vision.js', 'esm/vision.mjs', 'types/vision.d.ts', 'src/redact.js', 'esm/redact.mjs', 'types/redact.d.ts', 'docs/redact.md', 'src/share.js', 'src/share/store.js', 'esm/share.mjs', 'types/share.d.ts', 'docs/share.md', 'src/locker.js', 'src/locker/seal.js', 'src/locker/server.js', 'esm/locker.mjs', 'esm/locker-server.mjs', 'types/locker.d.ts', 'types/locker-server.d.ts', 'types/locker-vectors.d.ts', 'esm/locker-vectors.mjs', 'docs/locker.md', 'src/streams.js', 'esm/streams.mjs', 'types/streams.d.ts', 'docs/streams.md', 'browser/openvibe-sdk.mjs']) {++ for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/vibes.js', 'esm/vibes.mjs', 'types/vibes.d.ts', 'src/exchange.js', 'esm/exchange.mjs', 'types/exchange.d.ts', 'docs/exchange.md', 'src/bot.js', 'esm/bot.mjs', 'types/bot.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'src/cam/index.js', 'esm/cam.mjs', 'types/cam.d.ts', 'src/service/index.js', 'esm/service.mjs', 'types/service.d.ts', 'docs/service.md', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'src/resources.js', 'esm/resources.mjs', 'types/resources.d.ts', 'src/vision.js', 'esm/vision.mjs', 'types/vision.d.ts', 'src/redact.js', 'esm/redact.mjs', 'types/redact.d.ts', 'docs/redact.md', 'src/share.js', 'src/share/store.js', 'esm/share.mjs', 'types/share.d.ts', 'docs/share.md', 'src/locker.js', 'src/locker/seal.js', 'src/locker/server.js', 'esm/locker.mjs', 'esm/locker-server.mjs', 'types/locker.d.ts', 'types/locker-server.d.ts', 'types/locker-vectors.d.ts', 'esm/locker-vectors.mjs', 'docs/locker.md', 'src/streams.js', 'esm/streams.mjs', 'types/streams.d.ts', 'docs/streams.md', 'browser/openvibe-sdk.mjs']) {assert.ok(files.includes(must), `tarball contains ${must}`);}assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');@@@ -68,7 +68,7 @@ run([})().catch((err) => { console.error(err); process.exit(1); });`;const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));-- assert.equal(subpaths.length, 86); // 0.90.0 assertions, 0.91.0 streams, 0.92.0 chat-apps, 0.94.0 reports++ assert.equal(subpaths.length, 87); // 0.90.0 assertions, 0.91.0 streams, 0.92.0 chat-apps, 0.94.0 reports, 0.95.0 camassert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');} finally {fs.rmSync(tmp, { recursive: true, force: true });nnew file mode 100644@@@ -0,0 +1,117 @@++/**++ * openvibe-sdk/cam: the <ov-camera> element from OpenVibe.Cam.++ *++ * The element is defined by https://openvibe.cam/embed/ov-camera.js (or a self-hosted copy); this module++ * injects it and gives the attributes, events and methods a type.++ */++++/** The embed script the loader injects when no `src` is given. */++export declare const OV_CAMERA_SRC: string;++++/** The front camera (`user`) or the back camera (`environment`). */++export type OvCameraFacing = 'user' | 'environment';++++/** The filters the element knows; `filters` is a comma list of these names. */++export type OvCameraFilter = 'none' | 'mono' | 'warm' | 'cool' | 'fade' | 'vivid' | 'blur';++++/** A countdown before the capture: off, three seconds or ten. */++export type OvCameraTimer = 0 | 3 | 10;++++/** One photo, or a burst of three 450 ms apart. */++export type OvCameraMode = 'photo' | 'burst';++++/** `off` keeps every picture in the page; `openvibe` posts to Cam, only on openvibe.cam and signed in. */++export type OvCameraUpload = 'off' | 'openvibe';++++/** The attributes on the tag. Every one is optional; the defaults are in the comment. */++export interface OvCameraAttributes {++ /** `user` (the front camera, the default) or `environment` (the back camera). */++ facing?: OvCameraFacing;++ /** A comma list of {@link OvCameraFilter} names; unknown names are dropped and an empty list is `none`. */++ filters?: string;++ /** A countdown in seconds; anything other than 0, 3 or 10 is 0. Default 0. */++ timer?: OvCameraTimer | '0' | '3' | '10';++ /** Default `photo`. */++ mode?: OvCameraMode;++ /** Default `off`: the blob is only delivered as an event. */++ upload?: OvCameraUpload;++}++++/** The `detail` of `ov-camera:capture`, and what `capture()` resolves to. */++export interface OvCameraCaptureDetail {++ /** The encoded picture (`image/jpeg`, quality 0.92). */++ blob: Blob;++ width: number;++ height: number;++ /** The selected filter name ({@link OvCameraFilter}). */++ filter: string;++}++++export type OvCameraErrorCode =++ | 'camera_unavailable'++ | 'permission_denied'++ | 'not_started'++ | 'capture_failed'++ | 'file_unreadable'++ | 'upload_unavailable'++ | 'upload_denied'++ | 'upload_failed';++++/** The `detail` of `ov-camera:error`. */++export interface OvCameraErrorDetail {++ code: OvCameraErrorCode;++ message: string;++}++++export interface OvCameraCaptureEvent extends CustomEvent<OvCameraCaptureDetail> {++ type: 'ov-camera:capture';++}++++export interface OvCameraErrorEvent extends CustomEvent<OvCameraErrorDetail> {++ type: 'ov-camera:error';++}++++/** The events the element fires, both bubbling out of its shadow root. */++export interface OvCameraEventMap {++ 'ov-camera:capture': OvCameraCaptureEvent;++ 'ov-camera:error': OvCameraErrorEvent;++}++++/** The element itself, as `customElements.get('ov-camera')` returns it. */++export interface OvCameraElement extends HTMLElement {++ /** Ask for the camera. It is never called on connect; a click is the safe place to call it. */++ start(): Promise<void>;++ /** Stop every track and show the off card again. */++ stop(): void;++ /** Take a photo through the shutter path, honouring `timer` and `mode`; null when there is no camera. */++ capture(): Promise<OvCameraCaptureDetail | OvCameraCaptureDetail[] | null>;++ /** Switch front/back and restart the camera if it is on; resolves to the new facing. */++ flip(): Promise<OvCameraFacing>;++ addEventListener<K extends keyof OvCameraEventMap>(++ type: K,++ listener: (this: OvCameraElement, ev: OvCameraEventMap[K]) => void,++ options?: boolean | AddEventListenerOptions,++ ): void;++ addEventListener(++ type: string,++ listener: EventListenerOrEventListenerObject,++ options?: boolean | AddEventListenerOptions,++ ): void;++}++++export interface DefineOvCameraOptions {++ /** The embed script to inject; defaults to {@link OV_CAMERA_SRC}. */++ src?: string;++}++++/**++ * Inject the element's script once and resolve to its class. A second call reuses the load; a page that++ * already defines <ov-camera> resolves without a new tag.++ */++export declare function defineOvCamera(options?: DefineOvCameraOptions): Promise<CustomElementConstructor>;++++declare global {++ interface HTMLElementTagNameMap {++ 'ov-camera': OvCameraElement;++ }++}
@@ -1,5 +1,15 @@
@@ -1,5 +1,15 @@
# Changelog
# Changelog
+## 0.95.0 (2026-11-07)
+
+- Add `openvibe-sdk/cam`: `defineOvCamera({ src })` injects OpenVibe.Cam's framework-free `<ov-camera>` element
+ once (default `https://openvibe.cam/embed/ov-camera.js`) and resolves to the class. It is browser-only by nature:
+ calling it in Node rejects instead of touching `document`. `OV_CAMERA_SRC` is the default URL.
+- Type the element: `OvCameraElement` (`start()`, `stop()`, `capture()`, `flip()`), `OvCameraAttributes` (facing,
+ filters, timer, mode, upload), `OvCameraCaptureDetail` / `OvCameraErrorDetail`, the two event types and the
+ `HTMLElementTagNameMap` entry. `upload="off"` is still the default: a capture is a `Blob` in the page until the
+ site sends it somewhere.
+
## 0.94.0 (2026-11-07)
## 0.94.0 (2026-11-07)
- `openvibe-sdk/streams` gains S7: `schedule()`, `listSchedules()`, `cancelSchedule()` and `remindSchedule()` for
- `openvibe-sdk/streams` gains S7: `schedule()`, `listSchedules()`, `cancelSchedule()` and `remindSchedule()` for
@@ -2,16 +2,16 @@
@@ -2,16 +2,16 @@
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.94.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
+**Status:** alpha, v0.95.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
-After v0.94.0 is tagged:
+After v0.95.0 is tagged:
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.95.0
```
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
@@ -488,6 +488,31 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr
@@ -488,6 +488,31 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr
`browser/openvibe-sdk.mjs` is generated by `node scripts/browser-bundle.js` from the CommonJS sources reachable from `browser.js` (no dependencies, no transpiling) and checked in. It exports core at the top level and `auth` (browser build), `registry`, `modules`, `realtime`, `media`, `community`, `bot`, `jobs`, `tools` and `projects` as namespaces. `test/bundle.test.js` fails when it is stale, and the browser secret scan covers it.
`browser/openvibe-sdk.mjs` is generated by `node scripts/browser-bundle.js` from the CommonJS sources reachable from `browser.js` (no dependencies, no transpiling) and checked in. It exports core at the top level and `auth` (browser build), `registry`, `modules`, `realtime`, `media`, `community`, `bot`, `jobs`, `tools` and `projects` as namespaces. `test/bundle.test.js` fails when it is stale, and the browser secret scan covers it.
+### The OpenVibe camera element
+
+`openvibe-sdk/cam` loads the camera app's framework-free `<ov-camera>` element from OpenVibe.Cam and types its
+attributes, events and methods. It injects the embed script once and resolves to the element class:
+
+```js
+import { defineOvCamera } from 'openvibe-sdk/cam';
+
+await defineOvCamera(); // https://openvibe.cam/embed/ov-camera.js, once
+const camera = document.querySelector('ov-camera');
+camera.addEventListener('ov-camera:capture', (event) => {
+ // event.detail = { blob, width, height, filter }; nothing was uploaded
+ upload(event.detail.blob);
+});
+await camera.start(); // or let the person click the element's own button
+```
+
+```html
+<ov-camera facing="user" filters="none,mono,warm" timer="3" mode="photo" upload="off"></ov-camera>
+```
+
+`upload="off"` (the default) keeps the picture in the page. `upload="openvibe"` posts it to `POST /api/v1/photos` as
+the signed-in person, and only on openvibe.cam. `defineOvCamera({ src })` loads a self-hosted copy of the same file.
+The full attribute, event and method reference is at [openvibe.cam/developers](https://openvibe.cam/developers).
+
## Versioning
## Versioning
- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.
- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.
@@ -3,12 +3,13 @@
@@ -3,12 +3,13 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.94.0",
+ "version": "0.95.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
"contracts": "openvibe-contracts v0.312.0",
"contracts": "openvibe-contracts v0.312.0",
"features": [
"features": [
+ "cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
"chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.94.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.95.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, locker, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, locker, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -48,7 +48,7 @@ const { paginate, offsetPager } = __load(7);
@@ -48,7 +48,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.94.0';
+const SDK_VERSION = '0.95.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -479,6 +479,10 @@
@@ -479,6 +479,10 @@
"openvibe-sdk/cache": {
"openvibe-sdk/cache": {
"createCache": "function"
"createCache": "function"
},
},
+ "openvibe-sdk/cam": {
+ "OV_CAMERA_SRC": "string",
+ "defineOvCamera": "function"
+ },
"openvibe-sdk/chat": {
"openvibe-sdk/chat": {
"createChatClient": "function",
"createChatClient": "function",
"createFeedPublisher": "function",
"createFeedPublisher": "function",
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/cam/index.js. Do not edit.
+import cjs from '../src/cam/index.js';
+
+export const {
+ defineOvCamera, OV_CAMERA_SRC,
+} = cjs;
+export default cjs;
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.94.0",
+ "version": "0.95.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.94.0",
+ "version": "0.95.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.94.0",
+ "version": "0.95.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -347,6 +347,11 @@
@@ -347,6 +347,11 @@
"import": "./esm/app-signin.mjs",
"import": "./esm/app-signin.mjs",
"default": "./src/app-signin/index.js"
"default": "./src/app-signin/index.js"
},
},
+ "./cam": {
+ "types": "./types/cam.d.ts",
+ "import": "./esm/cam.mjs",
+ "default": "./src/cam/index.js"
+ },
"./run": {
"./run": {
"types": "./types/run.d.ts",
"types": "./types/run.d.ts",
"browser": null,
"browser": null,
@@ -28,6 +28,7 @@ const ENTRIES = {
@@ -28,6 +28,7 @@ const ENTRIES = {
'events.mjs': '../src/events.js',
'events.mjs': '../src/events.js',
'sso.mjs': '../src/sso/index.js',
'sso.mjs': '../src/sso/index.js',
'app-signin.mjs': '../src/app-signin/index.js',
'app-signin.mjs': '../src/app-signin/index.js',
+ 'cam.mjs': '../src/cam/index.js',
'realtime.mjs': '../src/realtime.js',
'realtime.mjs': '../src/realtime.js',
'media.mjs': '../src/media.js',
'media.mjs': '../src/media.js',
'community.mjs': '../src/community.js',
'community.mjs': '../src/community.js',
new file mode 100644
new file mode 100644
@@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
+'use strict';
+/**
+ * openvibe-sdk/cam: the OpenVibe camera element, from https://openvibe.cam/embed/ov-camera.js.
+ *
+ * In an app: import { defineOvCamera } from the package's cam entry.
+ * await defineOvCamera();
+ * const camera = document.querySelector('ov-camera');
+ * camera.addEventListener('ov-camera:capture', (event) => upload(event.detail.blob));
+ *
+ * The embed itself is OpenVibe.Cam's framework-free custom element. This loader only injects one
+ * <script> per origin (the page may already include it) and resolves to the class once the browser
+ * has defined it. Nothing here touches the camera: the element starts only from a click or an
+ * explicit start(), and its default upload="off" means a capture never leaves the page.
+ *
+ * <ov-camera facing="user" filters="none,mono,warm" timer="3" mode="photo" upload="off"></ov-camera>
+ *
+ * Browser-only by nature; in Node the call rejects and the module still loads (the types and the
+ * pack test import it). Pass `src` to self-host the same element file.
+ */
+const OV_CAMERA_SRC = 'https://openvibe.cam/embed/ov-camera.js';
+
+// One promise per src: a second call while the script is in flight gets the same load, never a second tag.
+const pending = new Map();
+
+/**
+ * Inject the <ov-camera> embed once and resolve to the element class.
+ * @param {{ src?: string }} [options]
+ * @returns {Promise<CustomElementConstructor>}
+ */
+function defineOvCamera(options = {}) {
+ const src = String((options && options.src) || OV_CAMERA_SRC);
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
+ return Promise.reject(new Error('openvibe-sdk/cam: defineOvCamera() needs a browser with custom elements.'));
+ }
+ const defined = () => (window.customElements ? window.customElements.get('ov-camera') : undefined);
+ const already = defined();
+ if (already) return Promise.resolve(already);
+ if (pending.has(src)) return pending.get(src);
+
+ const load = new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = src;
+ script.async = true;
+ script.dataset.ovCamera = '';
+ script.addEventListener('load', () => {
+ const ctor = defined();
+ if (ctor) resolve(ctor);
+ else reject(new Error(`openvibe-sdk/cam: ${src} loaded but did not define <ov-camera>.`));
+ });
+ script.addEventListener('error', () => {
+ script.remove();
+ reject(new Error(`openvibe-sdk/cam: could not load ${src}`));
+ });
+ (document.head || document.documentElement).appendChild(script);
+ }).finally(() => pending.delete(src));
+
+ pending.set(src, load);
+ return load;
+}
+
+module.exports = { defineOvCamera, OV_CAMERA_SRC };
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.94.0';
+const SDK_VERSION = '0.95.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -78,7 +78,7 @@ run([
@@ -78,7 +78,7 @@ run([
assert.deepEqual(problems, []);
assert.deepEqual(problems, []);
assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './commerce', './consent', './cost', './coupons', './db', './deals', './delivery', './download', './events', './exchange', './food', './govern', './host', './identity', './inventory', './limits', './live', './news', './node', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);
assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './commerce', './consent', './cost', './coupons', './db', './deals', './delivery', './download', './events', './exchange', './food', './govern', './host', './identity', './inventory', './limits', './live', './news', './node', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);
const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windows
const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windows
- for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
+ for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/cam/index.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
assert.ok(rel.includes(must), `${must} is scanned`);
assert.ok(rel.includes(must), `${must} is scanned`);
}
}
for (const never of ['src/auth/tokens.js', 'src/auth/jwt.js', 'src/auth/oauth.js', 'src/events.js', 'src/identity.js', 'src/sso/index.js', 'src/app-signin/index.js', 'src/service/index.js', 'src/service-outbox.js', 'src/share.js', 'src/share/store.js', 'src/testing/index.js']) {
for (const never of ['src/auth/tokens.js', 'src/auth/jwt.js', 'src/auth/oauth.js', 'src/events.js', 'src/identity.js', 'src/sso/index.js', 'src/app-signin/index.js', 'src/service/index.js', 'src/service-outbox.js', 'src/share.js', 'src/share/store.js', 'src/testing/index.js']) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
+'use strict';
+/**
+ * openvibe-sdk/cam: the export resolves by name (CJS and ESM), the loader injects the embed once in a fake DOM
+ * and resolves to the element class, Node gets a rejection instead of a document crash, and the type file names
+ * the runtime exports and the whole element surface.
+ */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { run } = require('./helpers');
+
+const ROOT = path.join(__dirname, '..');
+const cam = require('../src/cam');
+const pkg = require('../package.json');
+
+run([
+ ['the entry resolves by name and names the loader and the default script', async () => {
+ assert.deepEqual(Object.keys(cam).sort(), ['OV_CAMERA_SRC', 'defineOvCamera']);
+ assert.equal(cam.OV_CAMERA_SRC, 'https://openvibe.cam/embed/ov-camera.js');
+ assert.equal(require('openvibe-sdk/cam'), cam);
+ assert.deepEqual(pkg.exports['./cam'], {
+ types: './types/cam.d.ts',
+ import: './esm/cam.mjs',
+ default: './src/cam/index.js',
+ });
+ const esm = await import('openvibe-sdk/cam');
+ assert.equal(esm.default, cam);
+ assert.equal(esm.OV_CAMERA_SRC, cam.OV_CAMERA_SRC);
+ assert.equal(typeof esm.defineOvCamera, 'function');
+ }],
+
+ ['in Node it rejects instead of touching a document', async () => {
+ assert.equal(typeof global.window, 'undefined');
+ await assert.rejects(cam.defineOvCamera(), /needs a browser/);
+ await assert.rejects(cam.defineOvCamera({ src: 'https://example.test/ov-camera.js' }), /needs a browser/);
+ }],
+
+ ['a browser gets one <script> per load and the class; a defined element resolves without another', async () => {
+ const appended = [];
+ const listeners = {};
+ let ctor = null;
+ const script = {
+ dataset: {},
+ addEventListener(type, fn) { listeners[type] = fn; },
+ remove() { this.removed = true; },
+ };
+ const hadWindow = Object.prototype.hasOwnProperty.call(global, 'window');
+ const hadDocument = Object.prototype.hasOwnProperty.call(global, 'document');
+ const oldWindow = global.window;
+ const oldDocument = global.document;
+ global.window = { customElements: { get: (name) => (name === 'ov-camera' ? ctor : undefined) } };
+ global.document = {
+ createElement: () => script,
+ head: {
+ appendChild: (s) => {
+ appended.push(s.src);
+ ctor = function OvCamera() {};
+ queueMicrotask(() => listeners.load());
+ },
+ },
+ documentElement: { appendChild: () => { throw new Error('head exists; documentElement must not be used'); } },
+ };
+ try {
+ const [a, b] = await Promise.all([cam.defineOvCamera(), cam.defineOvCamera()]);
+ assert.equal(appended.length, 1, 'two concurrent calls share one script');
+ assert.equal(a, ctor);
+ assert.equal(b, ctor);
+ assert.equal(script.dataset.ovCamera, '', 'the tag is marked as ours');
+ assert.equal(script.async, true);
+
+ const again = await cam.defineOvCamera();
+ assert.equal(again, ctor);
+ assert.equal(appended.length, 1, 'a defined element needs no second script');
+
+ // A custom src is honoured when the page has not defined the element yet.
+ ctor = null;
+ const custom = await cam.defineOvCamera({ src: 'https://cdn.example/ov-camera.js' });
+ assert.equal(typeof custom, 'function');
+ assert.deepEqual(appended, ['https://openvibe.cam/embed/ov-camera.js', 'https://cdn.example/ov-camera.js']);
+ } finally {
+ if (hadWindow) global.window = oldWindow; else delete global.window;
+ if (hadDocument) global.document = oldDocument; else delete global.document;
+ }
+ }],
+
+ ['the types file names the runtime exports and the element surface', async () => {
+ const dts = fs.readFileSync(path.join(ROOT, 'types/cam.d.ts'), 'utf8');
+ for (const name of [
+ 'OV_CAMERA_SRC', 'defineOvCamera', 'DefineOvCameraOptions',
+ 'OvCameraAttributes', 'OvCameraFacing', 'OvCameraFilter', 'OvCameraTimer', 'OvCameraMode', 'OvCameraUpload',
+ 'OvCameraElement', 'OvCameraCaptureDetail', 'OvCameraErrorCode', 'OvCameraErrorDetail',
+ 'OvCameraCaptureEvent', 'OvCameraErrorEvent', 'OvCameraEventMap', 'HTMLElementTagNameMap',
+ 'start(): Promise<void>', 'stop(): void', 'capture(): Promise<', 'flip(): Promise<OvCameraFacing>',
+ ]) assert.ok(dts.includes(name), name);
+ }],
+]);
@@ -24,7 +24,7 @@ run([
@@ -24,7 +24,7 @@ run([
const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8', shell });
const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8', shell });
const info = JSON.parse(out)[0];
const info = JSON.parse(out)[0];
const files = info.files.map((f) => f.path);
const files = info.files.map((f) => f.path);
- for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/vibes.js', 'esm/vibes.mjs', 'types/vibes.d.ts', 'src/exchange.js', 'esm/exchange.mjs', 'types/exchange.d.ts', 'docs/exchange.md', 'src/bot.js', 'esm/bot.mjs', 'types/bot.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'src/service/index.js', 'esm/service.mjs', 'types/service.d.ts', 'docs/service.md', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'src/resources.js', 'esm/resources.mjs', 'types/resources.d.ts', 'src/vision.js', 'esm/vision.mjs', 'types/vision.d.ts', 'src/redact.js', 'esm/redact.mjs', 'types/redact.d.ts', 'docs/redact.md', 'src/share.js', 'src/share/store.js', 'esm/share.mjs', 'types/share.d.ts', 'docs/share.md', 'src/locker.js', 'src/locker/seal.js', 'src/locker/server.js', 'esm/locker.mjs', 'esm/locker-server.mjs', 'types/locker.d.ts', 'types/locker-server.d.ts', 'types/locker-vectors.d.ts', 'esm/locker-vectors.mjs', 'docs/locker.md', 'src/streams.js', 'esm/streams.mjs', 'types/streams.d.ts', 'docs/streams.md', 'browser/openvibe-sdk.mjs']) {
+ for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/vibes.js', 'esm/vibes.mjs', 'types/vibes.d.ts', 'src/exchange.js', 'esm/exchange.mjs', 'types/exchange.d.ts', 'docs/exchange.md', 'src/bot.js', 'esm/bot.mjs', 'types/bot.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'src/cam/index.js', 'esm/cam.mjs', 'types/cam.d.ts', 'src/service/index.js', 'esm/service.mjs', 'types/service.d.ts', 'docs/service.md', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'src/resources.js', 'esm/resources.mjs', 'types/resources.d.ts', 'src/vision.js', 'esm/vision.mjs', 'types/vision.d.ts', 'src/redact.js', 'esm/redact.mjs', 'types/redact.d.ts', 'docs/redact.md', 'src/share.js', 'src/share/store.js', 'esm/share.mjs', 'types/share.d.ts', 'docs/share.md', 'src/locker.js', 'src/locker/seal.js', 'src/locker/server.js', 'esm/locker.mjs', 'esm/locker-server.mjs', 'types/locker.d.ts', 'types/locker-server.d.ts', 'types/locker-vectors.d.ts', 'esm/locker-vectors.mjs', 'docs/locker.md', 'src/streams.js', 'esm/streams.mjs', 'types/streams.d.ts', 'docs/streams.md', 'browser/openvibe-sdk.mjs']) {
assert.ok(files.includes(must), `tarball contains ${must}`);
assert.ok(files.includes(must), `tarball contains ${must}`);
}
}
assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');
assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');
@@ -68,7 +68,7 @@ run([
@@ -68,7 +68,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
- assert.equal(subpaths.length, 86); // 0.90.0 assertions, 0.91.0 streams, 0.92.0 chat-apps, 0.94.0 reports
+ assert.equal(subpaths.length, 87); // 0.90.0 assertions, 0.91.0 streams, 0.92.0 chat-apps, 0.94.0 reports, 0.95.0 cam
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
new file mode 100644
new file mode 100644
@@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
+/**
+ * openvibe-sdk/cam: the <ov-camera> element from OpenVibe.Cam.
+ *
+ * The element is defined by https://openvibe.cam/embed/ov-camera.js (or a self-hosted copy); this module
+ * injects it and gives the attributes, events and methods a type.
+ */
+
+/** The embed script the loader injects when no `src` is given. */
+export declare const OV_CAMERA_SRC: string;
+
+/** The front camera (`user`) or the back camera (`environment`). */
+export type OvCameraFacing = 'user' | 'environment';
+
+/** The filters the element knows; `filters` is a comma list of these names. */
+export type OvCameraFilter = 'none' | 'mono' | 'warm' | 'cool' | 'fade' | 'vivid' | 'blur';
+
+/** A countdown before the capture: off, three seconds or ten. */
+export type OvCameraTimer = 0 | 3 | 10;
+
+/** One photo, or a burst of three 450 ms apart. */
+export type OvCameraMode = 'photo' | 'burst';
+
+/** `off` keeps every picture in the page; `openvibe` posts to Cam, only on openvibe.cam and signed in. */
+export type OvCameraUpload = 'off' | 'openvibe';
+
+/** The attributes on the tag. Every one is optional; the defaults are in the comment. */
+export interface OvCameraAttributes {
+ /** `user` (the front camera, the default) or `environment` (the back camera). */
+ facing?: OvCameraFacing;
+ /** A comma list of {@link OvCameraFilter} names; unknown names are dropped and an empty list is `none`. */
+ filters?: string;
+ /** A countdown in seconds; anything other than 0, 3 or 10 is 0. Default 0. */
+ timer?: OvCameraTimer | '0' | '3' | '10';
+ /** Default `photo`. */
+ mode?: OvCameraMode;
+ /** Default `off`: the blob is only delivered as an event. */
+ upload?: OvCameraUpload;
+}
+
+/** The `detail` of `ov-camera:capture`, and what `capture()` resolves to. */
+export interface OvCameraCaptureDetail {
+ /** The encoded picture (`image/jpeg`, quality 0.92). */
+ blob: Blob;
+ width: number;
+ height: number;
+ /** The selected filter name ({@link OvCameraFilter}). */
+ filter: string;
+}
+
+export type OvCameraErrorCode =
+ | 'camera_unavailable'
+ | 'permission_denied'
+ | 'not_started'
+ | 'capture_failed'
+ | 'file_unreadable'
+ | 'upload_unavailable'
+ | 'upload_denied'
+ | 'upload_failed';
+
+/** The `detail` of `ov-camera:error`. */
+export interface OvCameraErrorDetail {
+ code: OvCameraErrorCode;
+ message: string;
+}
+
+export interface OvCameraCaptureEvent extends CustomEvent<OvCameraCaptureDetail> {
+ type: 'ov-camera:capture';
+}
+
+export interface OvCameraErrorEvent extends CustomEvent<OvCameraErrorDetail> {
+ type: 'ov-camera:error';
+}
+
+/** The events the element fires, both bubbling out of its shadow root. */
+export interface OvCameraEventMap {
+ 'ov-camera:capture': OvCameraCaptureEvent;
+ 'ov-camera:error': OvCameraErrorEvent;
+}
+
+/** The element itself, as `customElements.get('ov-camera')` returns it. */
+export interface OvCameraElement extends HTMLElement {
+ /** Ask for the camera. It is never called on connect; a click is the safe place to call it. */
+ start(): Promise<void>;
+ /** Stop every track and show the off card again. */
+ stop(): void;
+ /** Take a photo through the shutter path, honouring `timer` and `mode`; null when there is no camera. */
+ capture(): Promise<OvCameraCaptureDetail | OvCameraCaptureDetail[] | null>;
+ /** Switch front/back and restart the camera if it is on; resolves to the new facing. */
+ flip(): Promise<OvCameraFacing>;
+ addEventListener<K extends keyof OvCameraEventMap>(
+ type: K,
+ listener: (this: OvCameraElement, ev: OvCameraEventMap[K]) => void,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+}
+
+export interface DefineOvCameraOptions {
+ /** The embed script to inject; defaults to {@link OV_CAMERA_SRC}. */
+ src?: string;
+}
+
+/**
+ * Inject the element's script once and resolve to its class. A second call reuses the load; a page that
+ * already defines <ov-camera> resolves without a new tag.
+ */
+export declare function defineOvCamera(options?: DefineOvCameraOptions): Promise<CustomElementConstructor>;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ov-camera': OvCameraElement;
+ }
+}