openvibers/openvibe.sdk Commits
0.6.0: openvibe-sdk/tools (the Tools registry and run API) and jobs retry/references; README Tools section, CHANGELOG
Changes
@@@ -3,6 +3,71 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease may change an API and says so here.++## 0.6.0 (2026-09-23)++++The OpenVibe.Tools platform API (ADR-027, openvibe-contracts v0.33.0) as `openvibe-sdk/tools`, and++retry and result references in `openvibe-sdk/jobs`. The Tools routes are being built: the registry++(`GET /api/v1/tools…`) comes with Tools S3, the run API and the gateway's `/api/v1/jobs` facade with++Tools S6. Until they are deployed this client is tested against the contracts and the mock only.++++- **`createToolsClient(client, { baseUrl?, service = 'tools', audience = 'openvibe.tools', anonymousReads = true })`**++ -> `{ list, get, schema, run, jobs }`, on the gateway (the `tools` origin from the platform++ descriptor unless `baseUrl` is given). Also in `index.js`, `browser.js`, the ESM entry++ (`esm/tools.mjs`) and the browser bundle (`tools` namespace).++ - `list({ family, q, execution, api, status })` answers `tools.tool-list@1` (schemas as `$ref`);++ `get(id)` the descriptor (`tools.tool@1`, schemas embedded); `schema(id)` `{ $schema, $id,++ $defs: { input, output } }`. Both are `null` on 404. Registry reads send no token unless++ `anonymousReads: false`.++ - `run(id, input = {}, { files, waitMs, idempotencyKey, signal, timeoutMs })` posts++ `tools.run-request@1`. An inline tool, or a job that finished within `waitMs`, resolves to++ `{ state: 'succeeded', tool, result: { data | text | files }, took_ms, job?, location? }`. A job still++ queued or running (202, or a replay) resolves to `{ state, tool, job, location }`. Both carry++ `idempotencyKey` and `replayed`, and a non-enumerable `wait(opts)`: a finished run resolves to++ itself, and a job follows its events (`jobs.wait`) to the succeeded run.++ - A run that finished `failed` or `cancelled` throws **`ToolRunError`**, an `OpenVibeError` (same++ `name`, `isOpenVibeError` is true) carrying the tool's problem+json (`code`, `status` of the++ problem, `title`, `detail`, `errors`) plus `state`, `tool`, `job` and `run`; `isToolRunError()`.++ A request refused before the tool ran (404 `tools.tool.not_found` | `not_runnable`, 422++ `tools.input.invalid`, 401/403, 429, 503 `tools.tool.unavailable`) is a plain `OpenVibeError`.++ - The `Idempotency-Key` is always sent (generated when omitted), so the core client retries a++ 429 after its `Retry-After`, and 5xx, with the same key: a job is created once. `timeoutMs` per++ attempt is raised to `waitMs`, or to the tool's `limits.timeoutMs` once this client has read its++ descriptor, plus 10 s.++ - **Files**: uploads in the jobs client's shapes (a Blob/File, or `{ name, data, type? }`) go as++ multipart `file` parts; references `{ media_id }` (a Media object you may read) and++ `{ job_id, index }` (a result file of your own job) go in the JSON body's `files`, or, beside++ uploads, as a multipart `files` part holding their JSON. The tool gets uploads first, then++ references. Bad references are a `TypeError` before anything is sent.++ - `jobs` is `createJobsClient()` on the same origin and credentials (the gateway facade).++- **`openvibe-sdk/jobs`:** `retry(id)` -> `{ job, replayed }` (a failed job as a new one; asking++ again returns that same retry), `reference(id, ref)` and `unreference(id, ref)` -> the job (keep a++ succeeded result while `<service>:<kind>:<id>` points at it; `expires_at` is null meanwhile).++ **Default origin:** without `baseUrl` the jobs client uses the `tools` origin++ (https://openvibe.tools), whose gateway fronts every satellite's jobs once Tools S6 ships the++ facade. Until then pass the satellite (`baseUrl: 'https://img.openvibe.tools'`); an explicit++ `baseUrl` keeps working as before. `Job` types gain `tool`, `retry_of`, `retried_by`,++ `references` and `links.retry` / `links.retried_by`.++- **Mock platform (`openvibe-sdk/testing`):** `tools: true | { descriptors, handlers, mediaObjects, stepMs }`++ serves the registry and run routes on `origins.tools`, with the default tools `dns`, `jsonminify`,++ `png`, `port`, `yt` and `protectpdf`. It models caller tiers (probes need `tools.net.probe`, and++ job tools need a token because the mock has no browser sessions), the refusal codes, idempotent job++ runs, `wait_ms`, and file references. New: `addTool()`, `addMediaObject()`, `state.tools`. **Mock++ jobs changed** so that they answer `tools.job@1` exactly: `error` is problem+json (`type`,++ `title`, `status`, `code`, `detail`), not `{ code, detail }`; result files carry `sha256`,++ `storage: 'local'` and `media: null`. Views also gain `retry_of`, `retried_by`, `references`,++ `links.retry` and `expires_at`. Jobs now serve `POST /:id/retry` and `PUT|DELETE /:id/references/:ref`,++ and `origins.tools` acts as the gateway facade, so it also finds the satellites' jobs. A test that++ compared a failed mock job's `error` with `{ code, detail }` must compare those fields instead. The++ mock `fetch` now rejects on an aborted signal, as `fetch` does.++- **Contracts:** `openvibe-contracts` v0.33.0 is a devDependency (tarball tag pin, tests only; still no++ runtime or peer dependency). `test/tools.test.js` validates every run request and every answer++ with it. `types/contracts.d.ts` is re-copied from it: `ModuleNamespace` gains its doc comment, and++ it adds `ToolDescriptor`, `ToolList`, `ToolsRunRequest`, `ToolsRun`, `ToolsJob` and++ `ToolsJobRequest`. CI no longer clones Contracts v0.28.0.++- Types: `types/tools.d.ts` (`ToolsClient`, `ToolRunOutcome`, `ToolRunSucceeded`, `ToolRunPending`,++ `ToolRunError`, `ToolFileRef`, `ToolRunFile`, `ToolSchema`, `ToolListQuery`, `ToolRunOptions`),++ plus the jobs and testing additions.++## 0.5.0 (2026-09-23)Media's object API v2 (`/api/v2/:app/objects`) and its jobs (`/api/v2/:app/jobs`), wrapped as@@@ -2,17 +2,17 @@> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.5.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4). v0.5.0 is not tagged yet: the install line below stays on v0.4.0 until it is. Tested against local stub servers and the built-in mock platform only, never against the live platform. Production services pin three releases (2026-09-23): v0.4.0 in Live, Deals, News, Reviews, Tips, Trade and VIP; v0.3.1 in Codes; v0.2.2 in Network, Media, Blog, Wiki, Coupons, Host, OpenRe.Stream and Games. OpenVibe.Examples uses v0.4.0. CI is green from `654d4b2` (it now runs `npm ci`); the runs for the v0.3.0, v0.3.1 and v0.4.0 commits failed because `better-sqlite3` was not installed.++**Status:** alpha, v0.6.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API from the 2026-09-23 tools program). v0.6.0 is not tagged yet: the install line below stays on v0.5.0 until it is. The Tools registry, run API and jobs facade it wraps are still being built in OpenVibe.Tools (see [Tools](#tools)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Production services pin three releases (2026-09-23): v0.4.0 in Live, Deals, News, Reviews, Tips, Trade and VIP; v0.3.1 in Codes; v0.2.2 in Network, Media, Blog, Wiki, Coupons, Host, OpenRe.Stream and Games. OpenVibe.Examples uses v0.4.0. CI is green from `654d4b2` (it now runs `npm ci`); the runs for the v0.3.0, v0.3.1 and v0.4.0 commits failed because `better-sqlite3` was not installed.**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.```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.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`: it copies the contract types it uses (from Contracts v0.28.0, checked in CI).++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 Contracts v0.33.0, a devDependency the tests check them against).## Quick start@@@ -135,16 +135,21 @@ app.credential.client_secret; // shown ONCE: store it nowawait projects.grants.request(prj.id, app.id, 'media.object.upload');```--Run a Tools job and follow it across disconnects and restarts:++Run a tool, or a Tools job, and follow it across disconnects and restarts (see [Tools](#tools)):```js--const jobs = require('openvibe-sdk/jobs').createJobsClient(client, { baseUrl: 'https://img.openvibe.tools' });++const tools = require('openvibe-sdk/tools').createToolsClient(client);++const done = await (await tools.run('webp', {}, { files: [{ name: 'a.png', data: bytes }] })).wait();++++const jobs = tools.jobs; // or createJobsClient(client): the gateway's /api/v1/jobs facade (Tools S6)const { job } = await jobs.submit({ type: 'img.process', input: { tool: 'convert', format: 'webp' }, files: [{ name: 'a.png', data: bytes }], idempotencyKey });for await (const e of jobs.events(job.id, { lastEventId: saved })) saveId(e.id); // reconnects with Last-Event-IDconst res = await jobs.file(job.id, 0); // raw Response```--`test/testing.test.js`, `test/apps.test.js`, `test/app-events.test.js`, `test/mock-services.test.js`, `test/projects.test.js` and `test/jobs.test.js` run these flows end to end against `openvibe-sdk/testing`. What the live platform allows today (sandbox audiences, allowances, which capabilities are grantable) is in the Network doc above and in OpenVibe.Examples' README.++Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client at the satellite that runs the type: `createJobsClient(client, { baseUrl: 'https://img.openvibe.tools' })` (img, audio or docs).++++`test/testing.test.js`, `test/apps.test.js`, `test/app-events.test.js`, `test/mock-services.test.js`, `test/projects.test.js`, `test/jobs.test.js` and `test/tools.test.js` run these flows end to end against `openvibe-sdk/testing`. What the live platform allows today (sandbox audiences, allowances, which capabilities are grantable) is in the Network doc above and in OpenVibe.Examples' README.## API@@@ -159,10 +164,11 @@ const res = await jobs.file(job.id, 0);| `openvibe-sdk/realtime` | both | `subscribe(topics, onEvent, {lastEventId, onGap, …})`, `createRealtimeClient(client)`, `parseSSE(body)` || `openvibe-sdk/media` | both (credentials: server) | `createMediaClient(client, {app, apiKey?, actingUserId?})`: `files.upload/list/iterate/get/delete`; `mediaUrls(origin)` public URL helpers; `createObjectsClient({app, baseUrl?, tokenClient \| apiKey \| client})`: object API v2 `upload` (single or multipart by size, resumable), `resume`, `get`, `signedUrl`, `delete`, `list`, `iterate`, and `jobs.create/get/list/approve/cancel/wait`; Media's origin from the platform descriptor when `baseUrl` is omitted || `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |--| `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()` |++| `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `retry`, `reference`, `unreference`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()`. Default origin: the Tools gateway's jobs facade |++| `openvibe-sdk/tools` | both | `createToolsClient(client, {baseUrl?, anonymousReads?})`: `list({family, q, execution, api, status})`, `get(id)`, `schema(id)`, `run(id, input, {files, waitMs, idempotencyKey, signal, timeoutMs})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()` || `openvibe-sdk/projects` | both (user token) | `createProjectsClient(client)`: `catalog`, `list`, `create`, `get`, `update`, `archive`, `setAllowance`, `setEnvironmentPolicy`, `members.*`, `apps.*`, `credentials.list/rotate/revoke`, `grants.list/request/approve/deny/revoke`, `quotas.*`, `audit`, `iterateAudit` |--| `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media and Tools jobs on an in-process `fetch` |--| `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, projects |++| `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media, Tools jobs and the Tools platform API on an in-process `fetch` |++| `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, tools, projects |Server-only subpaths are declared `"browser": null` in the exports map, so a bundler refuses them in a browser build and doesn't ship them by accident.@@@ -230,6 +236,47 @@ app.post('/internal/events', express.raw({ type: 'application/json' }), (req, reTurn `requireV2` on once Events sends v2 to you: a replayed v1-only delivery then fails. The lower-level checks are `verifyDeliveryV2(raw, headers, secret, { toleranceSec, now })` (constant-time; also refuses a `X-OpenVibe-Timestamp` that differs from `t`) and `verifyDelivery(raw, signatureHeader, secret)` (v1, unchanged). Keep your clock in sync (NTP): the window is ±300 s both ways. In tests, `signDeliveryHeaders(raw, secret, { now })` returns the three headers as Events sends them.++### Tools++++`openvibe-sdk/tools` wraps the OpenVibe.Tools platform API (ADR-027, openvibe-contracts v0.33.0) on the gateway, `https://openvibe.tools`: every tool has a descriptor (`tools.tool@1`), and every tool with `api: true` runs through one route. **Status:** the registry routes come with Tools S3, the run API and the gateway's `/api/v1/jobs` facade with Tools S6 (the capabilities `tools.tool.read`, `tools.tool.run` and `tools.net.probe` stay `planned` until then). Until they are deployed, this client is tested against the contracts and the mock only.++++```js++const { createToolsClient, isToolRunError } = require('openvibe-sdk/tools');++const tools = createToolsClient(client); // client: createClient({ … }); a token is optional++++const { tools: list, count } = await tools.list({ family: 'img', api: true }); // tools.tool-list@1, schemas as $ref++const png = await tools.get('png'); // tools.tool@1 with schemas embedded, or null++const { $defs } = await tools.schema('png'); // { input, output }: what the $refs point at++++// An inline tool (execution client with a server engine, or sync) answers finished:++const min = await tools.run('jsonminify', { text: '{ "a": 1 }' });++min.result.text; // '{"a":1}'++++// A job tool answers 202 with its job; wait() follows its events to the result:++const run = await tools.run('png', {}, { files: [{ name: 'photo.jpg', data: bytes, type: 'image/jpeg' }] });++run.state; // 'queued' (run.job, run.location = /api/v1/jobs/job_…)++const done = await run.wait({ onEvent: (e) => show(e.job.progress) });++done.result.files[0]; // { name: 'photo.png', mime, size, sha256, url, … }++const bytesOut = await (await tools.jobs.file(done.job.id, 0)).arrayBuffer();++++// One tool's output feeds the next, without a download:++const webp = await (await tools.run('webp', {}, { files: [{ job_id: done.job.id, index: 0 }], waitMs: 10000 })).wait();++++try {++ await tools.run('jsonminify', { text: '{' });++} catch (err) {++ if (isToolRunError(err)) console.log(err.code, err.status, err.detail); // the tool's own problem+json++}++```++++- **What `run()` resolves to.** An inline tool, or a job that finished within `waitMs`, gives `{ state: 'succeeded', tool, result, took_ms, job?, location? }`. `result` carries `data` (output kind json), `text` (kind text) or `files` (kind file or files: the job's result files). A job still queued or running gives `{ state: 'queued' | 'running', tool, job, location }`. Both have `idempotencyKey`, `replayed` and a non-enumerable `wait(opts)`. On a finished run `wait()` resolves to the run itself, so `await (await tools.run(…)).wait()` works for every tool. On a job, `wait()` takes `jobs.wait`'s options (`signal`, `onEvent`, `lastEventId`), and `tools.jobs.wait(job.id)` gives the raw job.++- **Failures.** A run that finished `failed` or `cancelled` throws `ToolRunError`. It is an `OpenVibeError` with the tool's problem+json mapped: `code`, `status` (the problem's, e.g. 422 or 504, not the HTTP 200), `detail` and `errors`. It also carries `state`, `tool`, `job` and `run`. When `err.retryable`, `tools.jobs.retry(err.job.id)` runs the failed job again. A refusal before the tool ran is a plain `OpenVibeError`: 404 `tools.tool.not_found` or `tools.tool.not_runnable` (`api: false`: the YouTube downloader is page-only and never in the SDK), 422 `tools.input.invalid` (with `errors[]` pointers), 401 `token.*`, 403 `capability.denied`, 413/415 for files, 429 `quota.exceeded` or `tools.job.too_many_active`, 503 `tools.tool.unavailable`.++- **Idempotency and retries.** Every run carries an `Idempotency-Key`, generated when you pass none and returned as `idempotencyKey`. The same key and request give the same job (`replayed: true`); a different request under that key is 409 `tools.job.idempotency_conflict`. Inline tools ignore the key. Because of the key, the client retries a 429 after its `Retry-After` (capped by `maxRetryDelayMs`) and retries 5xx and timeouts without creating a second job. To survive a crash between the run and saving the job id, derive the key from the request and run again with it.++- **Files.** Uploads use the jobs client's shapes: a `Blob`/`File`, or `{ name, data: Buffer | Uint8Array | ArrayBuffer | Blob | string, type? }`. They go as multipart `file` parts. References are `{ media_id: 'med_…' }` (a Media object you may read) and `{ job_id: 'job_…', index }` (a result file of your own job). They go in the JSON body's `files`, or, beside uploads, as a multipart `files` part holding their JSON. The tool gets the uploads first, then the references in order, and the count must fit the descriptor's `files.min`/`files.max`.++- **Waiting and timeouts.** `waitMs` (0 to 60000) lets a job tool answer finished when it is quick. Each attempt's timeout is raised to `waitMs`, or to the tool's `limits.timeoutMs` once this client has read its descriptor (`get` or `list`), plus 10 s. Pass `timeoutMs` to set it yourself. `signal` aborts the run (`sdk.aborted`), and a `wait()` too. Aborting a wait leaves the job running.++- **Who may run what.** Callers are tiered anonymous < session < user < app/service. Without a token you run the tools whose descriptor says `auth.anonymous: true`, keyed by IP. A person's token runs every tool except network probes. An app or service token (audience `openvibe.tools`) needs the descriptor's `auth.capability`: `tools.tool.run` (public), or `tools.net.probe` (partner: staff grant it by hand). Quotas count each tool's `cost` within its `quotaClass`. Registry reads (`tools.tool.read`) are public and send no token unless you pass `anonymousReads: false`.++- **Jobs.** `tools.jobs` is `createJobsClient()` on the same origin and credentials: the gateway's `/api/v1/jobs` facade, which fronts every satellite. It adds `retry(id)`, which returns `{ job, replayed }` (a failed job as a new one, `retry_of`; asking again returns the same retry), and `reference(id, 'community:paste:p_123')` / `unreference(id, ref)`, which keep a succeeded result while something points at it (`expires_at: null`). Until the facade is deployed (Tools S6), use `createJobsClient(client, { baseUrl: 'https://img.openvibe.tools' })` (or `audio.`, `docs.`) for jobs.++### Testing your app```js@@@ -240,6 +287,7 @@ const platform = createMockPlatform({mediaApps: { 'my-app': {} },users: [{ username: 'ana' }],jobs: true,++ tools: true,});const client = createClient({ fetch: platform.fetch, tokenProvider: createServiceTokenClient({ clientId: 'app_01K5WZX7S7Q4D2B8N3M6V1C9TR', clientSecret: 's', fetch: platform.fetch }) });```@@@ -251,9 +299,10 @@ The mock answers at the real public origins with real RS256 tokens and checks au- **Sandbox:** as in production, Media (on `/api/v1/<project_id>/files`) and Events (on the `events.app.*` routes) accept sandbox app tokens and keep their data apart from production; every other route and mock service refuses `env: sandbox` tokens (`401 token.sandbox_refused`) unless `acceptSandbox` lists the audience or capability. Unlike Network, sandbox apps get tokens for any audience unless you pass `sandboxAudiences`, and projects created through the API start with the whole catalog as allowance unless you pass `defaultAllowance: []`.- **Events:** publish with `event_id` dedupe, pull (with a `gap` after `pruneEvents(seq)`), checkpoints, subscriptions, `/realtime/stream` SSE with `Last-Event-ID` and gap events, and a delivery worker: `deliverEvents()` / `startDeliveries()` POST signed deliveries (v1 and v2 headers, fresh timestamp per attempt) to your local endpoint in order, retry, and mark them dead after `max_attempts`. Developer apps follow OpenVibe.Events' rules: app tokens are judged only on `events.app.publish | read | subscribe`; types `app.<project_key>.<name…>`, source `app-<ulid>`, actor the app or its `on_behalf_of` user; reads, checkpoints and subscriptions limited to the own project in the token's env plus public first-party events (every pattern starts with a literal segment, `app.*` patterns name the own key); app endpoints https and not loopback, private or local names; first-party readers never see sandbox events and see app events only through `app.*`; realtime streams neither. Not modelled: per-project quotas, revocation, and the DNS half of the endpoint check. Your delivery worker's `fetch` (`deliverEvents({ fetch })`) can route `https://hooks.example.com/…` to a local server.- **Media:** the files API and `GET /f/:key`, with Media's tenant rules: `media.object.upload` uploads and deletes, `media.object.read` lists and gets (app keys, service tokens and app tokens alike); a developer app reaches only `/api/v1/<its project_id>/files`, where production uses the tenant `prj_…` and sandbox `prj_…-sandbox` (100 MB, `mediaQuotaMb`); sandbox files come back as `{ sandbox: true, url: <signed>, url_expires_at }` and `/f/<key>` serves them only with a valid signature.--- **Tools jobs** (`jobs: true | { stepMs, handlers }`) at `origins.tools` and the satellites `img.`, `audio.` and `docs.openvibe.tools` (`platform.toolsOrigins`; `toolsSatellites` overrides), each with its own jobs: submit with Idempotency-Key replay, get, cancel, SSE with `Last-Event-ID` and `204` when finished, result files; `dropJobStreams()` simulates a dropped connection.++- **Tools jobs** (`jobs: true | { stepMs, handlers }`) at `origins.tools` and the satellites `img.`, `audio.` and `docs.openvibe.tools` (`platform.toolsOrigins`; `toolsSatellites` overrides). Each satellite keeps its own jobs, and `origins.tools` is the gateway facade that also finds theirs. Covered: submit with Idempotency-Key replay, get, cancel, retry, references, SSE with `Last-Event-ID` and `204` when finished, and result files. Every view is `tools.job@1`, with `error` as problem+json. `dropJobStreams()` simulates a dropped connection.++- **Tools platform API** (`tools: true | { descriptors, handlers, mediaObjects, stepMs }`) on `origins.tools`: `GET /api/v1/tools[/:id[/schema]]` and `POST /api/v1/tools/:id/run`, answering as openvibe-contracts v0.33.0 says. The default tools are `dns` (sync), `jsonminify` (a client tool with a server engine), `png` (a job), `port` (a probe needing `tools.net.probe`), `yt` (page-only) and `protectpdf` (unavailable). `descriptors` adds or replaces tools, and `handlers` answers them (`{ data }` or `{ text }` inline, a job handler for job tools). Also: `addTool()`, `addMediaObject()` (what `{ media_id }` reads) and `state.tools`. The mock checks caller tiers, the refusal codes, the file count and type, and the input's top-level fields. Idempotent job runs, `wait_ms` and file references work. It has no browser sessions, so job tools need a token. It has no quotas either: to test a 429, wrap `platform.fetch`.--Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()`, `setAuthorization()`, `publishEvent(envelope, publisher, { projectId, env })` (a registered app's `app:<id>` publisher implies them), `pruneEvents()`, `deliverEvents()`, `dropRealtime()`, `dropJobStreams()`, and `stats` and `state` for assertions. It is a fake. It has no persistence, its visibility rules are simplified, and it has no Chat (no WebSocket mock).++Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()`, `setAuthorization()`, `publishEvent(envelope, publisher, { projectId, env })` (a registered app's `app:<id>` publisher implies them), `pruneEvents()`, `deliverEvents()`, `dropRealtime()`, `dropJobStreams()`, `addTool()`, `addMediaObject()`, and `stats` and `state` for assertions. Its `fetch` rejects on an aborted signal, as `fetch` does. It is a fake. It has no persistence, its visibility rules are simplified, and it has no Chat (no WebSocket mock).### Browser without a bundler@@@ -264,12 +313,12 @@ Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()</script>```--`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`, `jobs` 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`, `jobs`, `tools` and `projects` as namespaces. `test/bundle.test.js` fails when it is stale, and the browser secret scan covers it.## 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.--- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release. CI clones `OpenVibe.Contracts` at the pinned tag for this check.++- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release, the `openvibe-contracts` devDependency (a release tarball pin, v0.33.0), which the tools tests also use to validate requests and answers. It is never a runtime or peer dependency.- A new public capability ships as a minor release: first the contract, then the wrapper here, then the service's route. Removing a wrapper is a major release, after the capability's deprecation window in Contracts has passed.## Not wrapped yet (intentionally)@@@ -279,7 +328,7 @@ Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()- **Media from the browser:** Media refuses user JWTs on `/api/v1/:app/files`, so a browser uploads through its own app server. That server holds the app key and names the user with `actingUserId`.- **Network coins, notifications and legacy-map writes** are internal service-to-service routes with no public capability, so they are not wrapped. Staff-only paste routes are also internal: admin stats, bulk, censor and the AI pass.- **Chat, Live, Billing and Games** from the original charter come when those services publish their capabilities in Contracts. Chat has no app principal (`chat.message.send` is `first-party`), so there is no Chat client and no Chat mock.--- **Tools job types** are not wrapped one by one: `openvibe-sdk/jobs` submits any `type` with its `input` and files; each satellite (img, audio, docs) documents its own types.++- **Tools** are not wrapped one by one: `openvibe-sdk/tools` runs any tool with `api: true` by id, and its descriptor (`tools.get(id)`) gives the input schema, files and limits. `openvibe-sdk/jobs` still submits any job `type` directly. The YouTube downloader is page-only (`api: false`), so it is not in the SDK.- **Realtime over WebSocket and presence** don't exist in Events yet (ADR-005). SSE is the only transport.## Development@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.5.0",++ "version": "0.6.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","updated": "2026-09-23"}@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.5.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.6.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -37,7 +37,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.5.0';++const SDK_VERSION = '0.6.0';module.exports = {createClient,@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.5.0",++ "version": "0.6.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.5.0",++ "version": "0.6.0","license": "MIT","devDependencies": {"better-sqlite3": "^11.10.0",@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.5.0",++ "version": "0.6.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Tools (registry, run API and jobs) and developer projects, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -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.5.0';++const SDK_VERSION = '0.6.0';module.exports = {createClient,
@@ -3,6 +3,71 @@
@@ -3,6 +3,71 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
release may change an API and says so here.
+## 0.6.0 (2026-09-23)
+
+The OpenVibe.Tools platform API (ADR-027, openvibe-contracts v0.33.0) as `openvibe-sdk/tools`, and
+retry and result references in `openvibe-sdk/jobs`. The Tools routes are being built: the registry
+(`GET /api/v1/tools…`) comes with Tools S3, the run API and the gateway's `/api/v1/jobs` facade with
+Tools S6. Until they are deployed this client is tested against the contracts and the mock only.
+
+- **`createToolsClient(client, { baseUrl?, service = 'tools', audience = 'openvibe.tools', anonymousReads = true })`**
+ -> `{ list, get, schema, run, jobs }`, on the gateway (the `tools` origin from the platform
+ descriptor unless `baseUrl` is given). Also in `index.js`, `browser.js`, the ESM entry
+ (`esm/tools.mjs`) and the browser bundle (`tools` namespace).
+ - `list({ family, q, execution, api, status })` answers `tools.tool-list@1` (schemas as `$ref`);
+ `get(id)` the descriptor (`tools.tool@1`, schemas embedded); `schema(id)` `{ $schema, $id,
+ $defs: { input, output } }`. Both are `null` on 404. Registry reads send no token unless
+ `anonymousReads: false`.
+ - `run(id, input = {}, { files, waitMs, idempotencyKey, signal, timeoutMs })` posts
+ `tools.run-request@1`. An inline tool, or a job that finished within `waitMs`, resolves to
+ `{ state: 'succeeded', tool, result: { data | text | files }, took_ms, job?, location? }`. A job still
+ queued or running (202, or a replay) resolves to `{ state, tool, job, location }`. Both carry
+ `idempotencyKey` and `replayed`, and a non-enumerable `wait(opts)`: a finished run resolves to
+ itself, and a job follows its events (`jobs.wait`) to the succeeded run.
+ - A run that finished `failed` or `cancelled` throws **`ToolRunError`**, an `OpenVibeError` (same
+ `name`, `isOpenVibeError` is true) carrying the tool's problem+json (`code`, `status` of the
+ problem, `title`, `detail`, `errors`) plus `state`, `tool`, `job` and `run`; `isToolRunError()`.
+ A request refused before the tool ran (404 `tools.tool.not_found` | `not_runnable`, 422
+ `tools.input.invalid`, 401/403, 429, 503 `tools.tool.unavailable`) is a plain `OpenVibeError`.
+ - The `Idempotency-Key` is always sent (generated when omitted), so the core client retries a
+ 429 after its `Retry-After`, and 5xx, with the same key: a job is created once. `timeoutMs` per
+ attempt is raised to `waitMs`, or to the tool's `limits.timeoutMs` once this client has read its
+ descriptor, plus 10 s.
+ - **Files**: uploads in the jobs client's shapes (a Blob/File, or `{ name, data, type? }`) go as
+ multipart `file` parts; references `{ media_id }` (a Media object you may read) and
+ `{ job_id, index }` (a result file of your own job) go in the JSON body's `files`, or, beside
+ uploads, as a multipart `files` part holding their JSON. The tool gets uploads first, then
+ references. Bad references are a `TypeError` before anything is sent.
+ - `jobs` is `createJobsClient()` on the same origin and credentials (the gateway facade).
+- **`openvibe-sdk/jobs`:** `retry(id)` -> `{ job, replayed }` (a failed job as a new one; asking
+ again returns that same retry), `reference(id, ref)` and `unreference(id, ref)` -> the job (keep a
+ succeeded result while `<service>:<kind>:<id>` points at it; `expires_at` is null meanwhile).
+ **Default origin:** without `baseUrl` the jobs client uses the `tools` origin
+ (https://openvibe.tools), whose gateway fronts every satellite's jobs once Tools S6 ships the
+ facade. Until then pass the satellite (`baseUrl: 'https://img.openvibe.tools'`); an explicit
+ `baseUrl` keeps working as before. `Job` types gain `tool`, `retry_of`, `retried_by`,
+ `references` and `links.retry` / `links.retried_by`.
+- **Mock platform (`openvibe-sdk/testing`):** `tools: true | { descriptors, handlers, mediaObjects, stepMs }`
+ serves the registry and run routes on `origins.tools`, with the default tools `dns`, `jsonminify`,
+ `png`, `port`, `yt` and `protectpdf`. It models caller tiers (probes need `tools.net.probe`, and
+ job tools need a token because the mock has no browser sessions), the refusal codes, idempotent job
+ runs, `wait_ms`, and file references. New: `addTool()`, `addMediaObject()`, `state.tools`. **Mock
+ jobs changed** so that they answer `tools.job@1` exactly: `error` is problem+json (`type`,
+ `title`, `status`, `code`, `detail`), not `{ code, detail }`; result files carry `sha256`,
+ `storage: 'local'` and `media: null`. Views also gain `retry_of`, `retried_by`, `references`,
+ `links.retry` and `expires_at`. Jobs now serve `POST /:id/retry` and `PUT|DELETE /:id/references/:ref`,
+ and `origins.tools` acts as the gateway facade, so it also finds the satellites' jobs. A test that
+ compared a failed mock job's `error` with `{ code, detail }` must compare those fields instead. The
+ mock `fetch` now rejects on an aborted signal, as `fetch` does.
+- **Contracts:** `openvibe-contracts` v0.33.0 is a devDependency (tarball tag pin, tests only; still no
+ runtime or peer dependency). `test/tools.test.js` validates every run request and every answer
+ with it. `types/contracts.d.ts` is re-copied from it: `ModuleNamespace` gains its doc comment, and
+ it adds `ToolDescriptor`, `ToolList`, `ToolsRunRequest`, `ToolsRun`, `ToolsJob` and
+ `ToolsJobRequest`. CI no longer clones Contracts v0.28.0.
+- Types: `types/tools.d.ts` (`ToolsClient`, `ToolRunOutcome`, `ToolRunSucceeded`, `ToolRunPending`,
+ `ToolRunError`, `ToolFileRef`, `ToolRunFile`, `ToolSchema`, `ToolListQuery`, `ToolRunOptions`),
+ plus the jobs and testing additions.
+
## 0.5.0 (2026-09-23)
## 0.5.0 (2026-09-23)
Media's object API v2 (`/api/v2/:app/objects`) and its jobs (`/api/v2/:app/jobs`), wrapped as
Media's object API v2 (`/api/v2/:app/objects`) and its jobs (`/api/v2/:app/jobs`), wrapped as
@@ -2,17 +2,17 @@
@@ -2,17 +2,17 @@
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.5.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4). v0.5.0 is not tagged yet: the install line below stays on v0.4.0 until it is. Tested against local stub servers and the built-in mock platform only, never against the live platform. Production services pin three releases (2026-09-23): v0.4.0 in Live, Deals, News, Reviews, Tips, Trade and VIP; v0.3.1 in Codes; v0.2.2 in Network, Media, Blog, Wiki, Coupons, Host, OpenRe.Stream and Games. OpenVibe.Examples uses v0.4.0. CI is green from `654d4b2` (it now runs `npm ci`); the runs for the v0.3.0, v0.3.1 and v0.4.0 commits failed because `better-sqlite3` was not installed.
+**Status:** alpha, v0.6.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API from the 2026-09-23 tools program). v0.6.0 is not tagged yet: the install line below stays on v0.5.0 until it is. The Tools registry, run API and jobs facade it wraps are still being built in OpenVibe.Tools (see [Tools](#tools)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Production services pin three releases (2026-09-23): v0.4.0 in Live, Deals, News, Reviews, Tips, Trade and VIP; v0.3.1 in Codes; v0.2.2 in Network, Media, Blog, Wiki, Coupons, Host, OpenRe.Stream and Games. OpenVibe.Examples uses v0.4.0. CI is green from `654d4b2` (it now runs `npm ci`); the runs for the v0.3.0, v0.3.1 and v0.4.0 commits failed because `better-sqlite3` was not installed.
**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.
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.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`: it copies the contract types it uses (from Contracts v0.28.0, checked in CI).
+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 Contracts v0.33.0, a devDependency the tests check them against).
## Quick start
## Quick start
@@ -135,16 +135,21 @@ app.credential.client_secret; // shown ONCE: store it now
@@ -135,16 +135,21 @@ app.credential.client_secret; // shown ONCE: store it now
await projects.grants.request(prj.id, app.id, 'media.object.upload');
await projects.grants.request(prj.id, app.id, 'media.object.upload');
```
```
-Run a Tools job and follow it across disconnects and restarts:
+Run a tool, or a Tools job, and follow it across disconnects and restarts (see [Tools](#tools)):
```js
```js
-const jobs = require('openvibe-sdk/jobs').createJobsClient(client, { baseUrl: 'https://img.openvibe.tools' });
+const tools = require('openvibe-sdk/tools').createToolsClient(client);
+const done = await (await tools.run('webp', {}, { files: [{ name: 'a.png', data: bytes }] })).wait();
+
+const jobs = tools.jobs; // or createJobsClient(client): the gateway's /api/v1/jobs facade (Tools S6)
const { job } = await jobs.submit({ type: 'img.process', input: { tool: 'convert', format: 'webp' }, files: [{ name: 'a.png', data: bytes }], idempotencyKey });
const { job } = await jobs.submit({ type: 'img.process', input: { tool: 'convert', format: 'webp' }, files: [{ name: 'a.png', data: bytes }], idempotencyKey });
for await (const e of jobs.events(job.id, { lastEventId: saved })) saveId(e.id); // reconnects with Last-Event-ID
for await (const e of jobs.events(job.id, { lastEventId: saved })) saveId(e.id); // reconnects with Last-Event-ID
const res = await jobs.file(job.id, 0); // raw Response
const res = await jobs.file(job.id, 0); // raw Response
```
```
-`test/testing.test.js`, `test/apps.test.js`, `test/app-events.test.js`, `test/mock-services.test.js`, `test/projects.test.js` and `test/jobs.test.js` run these flows end to end against `openvibe-sdk/testing`. What the live platform allows today (sandbox audiences, allowances, which capabilities are grantable) is in the Network doc above and in OpenVibe.Examples' README.
+Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client at the satellite that runs the type: `createJobsClient(client, { baseUrl: 'https://img.openvibe.tools' })` (img, audio or docs).
+
+`test/testing.test.js`, `test/apps.test.js`, `test/app-events.test.js`, `test/mock-services.test.js`, `test/projects.test.js`, `test/jobs.test.js` and `test/tools.test.js` run these flows end to end against `openvibe-sdk/testing`. What the live platform allows today (sandbox audiences, allowances, which capabilities are grantable) is in the Network doc above and in OpenVibe.Examples' README.
## API
## API
@@ -159,10 +164,11 @@ const res = await jobs.file(job.id, 0);
@@ -159,10 +164,11 @@ const res = await jobs.file(job.id, 0);
| `openvibe-sdk/realtime` | both | `subscribe(topics, onEvent, {lastEventId, onGap, …})`, `createRealtimeClient(client)`, `parseSSE(body)` |
| `openvibe-sdk/realtime` | both | `subscribe(topics, onEvent, {lastEventId, onGap, …})`, `createRealtimeClient(client)`, `parseSSE(body)` |
| `openvibe-sdk/media` | both (credentials: server) | `createMediaClient(client, {app, apiKey?, actingUserId?})`: `files.upload/list/iterate/get/delete`; `mediaUrls(origin)` public URL helpers; `createObjectsClient({app, baseUrl?, tokenClient \| apiKey \| client})`: object API v2 `upload` (single or multipart by size, resumable), `resume`, `get`, `signedUrl`, `delete`, `list`, `iterate`, and `jobs.create/get/list/approve/cancel/wait`; Media's origin from the platform descriptor when `baseUrl` is omitted |
| `openvibe-sdk/media` | both (credentials: server) | `createMediaClient(client, {app, apiKey?, actingUserId?})`: `files.upload/list/iterate/get/delete`; `mediaUrls(origin)` public URL helpers; `createObjectsClient({app, baseUrl?, tokenClient \| apiKey \| client})`: object API v2 `upload` (single or multipart by size, resumable), `resume`, `get`, `signedUrl`, `delete`, `list`, `iterate`, and `jobs.create/get/list/approve/cancel/wait`; Media's origin from the platform descriptor when `baseUrl` is omitted |
| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |
| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |
-| `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()` |
+| `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `retry`, `reference`, `unreference`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()`. Default origin: the Tools gateway's jobs facade |
+| `openvibe-sdk/tools` | both | `createToolsClient(client, {baseUrl?, anonymousReads?})`: `list({family, q, execution, api, status})`, `get(id)`, `schema(id)`, `run(id, input, {files, waitMs, idempotencyKey, signal, timeoutMs})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()` |
| `openvibe-sdk/projects` | both (user token) | `createProjectsClient(client)`: `catalog`, `list`, `create`, `get`, `update`, `archive`, `setAllowance`, `setEnvironmentPolicy`, `members.*`, `apps.*`, `credentials.list/rotate/revoke`, `grants.list/request/approve/deny/revoke`, `quotas.*`, `audit`, `iterateAudit` |
| `openvibe-sdk/projects` | both (user token) | `createProjectsClient(client)`: `catalog`, `list`, `create`, `get`, `update`, `archive`, `setAllowance`, `setEnvironmentPolicy`, `members.*`, `apps.*`, `credentials.list/rotate/revoke`, `grants.list/request/approve/deny/revoke`, `quotas.*`, `audit`, `iterateAudit` |
-| `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media and Tools jobs on an in-process `fetch` |
-| `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, projects |
+| `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media, Tools jobs and the Tools platform API on an in-process `fetch` |
+| `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, tools, projects |
Server-only subpaths are declared `"browser": null` in the exports map, so a bundler refuses them in a browser build and doesn't ship them by accident.
Server-only subpaths are declared `"browser": null` in the exports map, so a bundler refuses them in a browser build and doesn't ship them by accident.
@@ -230,6 +236,47 @@ app.post('/internal/events', express.raw({ type: 'application/json' }), (req, re
@@ -230,6 +236,47 @@ app.post('/internal/events', express.raw({ type: 'application/json' }), (req, re
Turn `requireV2` on once Events sends v2 to you: a replayed v1-only delivery then fails. The lower-level checks are `verifyDeliveryV2(raw, headers, secret, { toleranceSec, now })` (constant-time; also refuses a `X-OpenVibe-Timestamp` that differs from `t`) and `verifyDelivery(raw, signatureHeader, secret)` (v1, unchanged). Keep your clock in sync (NTP): the window is ±300 s both ways. In tests, `signDeliveryHeaders(raw, secret, { now })` returns the three headers as Events sends them.
Turn `requireV2` on once Events sends v2 to you: a replayed v1-only delivery then fails. The lower-level checks are `verifyDeliveryV2(raw, headers, secret, { toleranceSec, now })` (constant-time; also refuses a `X-OpenVibe-Timestamp` that differs from `t`) and `verifyDelivery(raw, signatureHeader, secret)` (v1, unchanged). Keep your clock in sync (NTP): the window is ±300 s both ways. In tests, `signDeliveryHeaders(raw, secret, { now })` returns the three headers as Events sends them.
+### Tools
+
+`openvibe-sdk/tools` wraps the OpenVibe.Tools platform API (ADR-027, openvibe-contracts v0.33.0) on the gateway, `https://openvibe.tools`: every tool has a descriptor (`tools.tool@1`), and every tool with `api: true` runs through one route. **Status:** the registry routes come with Tools S3, the run API and the gateway's `/api/v1/jobs` facade with Tools S6 (the capabilities `tools.tool.read`, `tools.tool.run` and `tools.net.probe` stay `planned` until then). Until they are deployed, this client is tested against the contracts and the mock only.
+
+```js
+const { createToolsClient, isToolRunError } = require('openvibe-sdk/tools');
+const tools = createToolsClient(client); // client: createClient({ … }); a token is optional
+
+const { tools: list, count } = await tools.list({ family: 'img', api: true }); // tools.tool-list@1, schemas as $ref
+const png = await tools.get('png'); // tools.tool@1 with schemas embedded, or null
+const { $defs } = await tools.schema('png'); // { input, output }: what the $refs point at
+
+// An inline tool (execution client with a server engine, or sync) answers finished:
+const min = await tools.run('jsonminify', { text: '{ "a": 1 }' });
+min.result.text; // '{"a":1}'
+
+// A job tool answers 202 with its job; wait() follows its events to the result:
+const run = await tools.run('png', {}, { files: [{ name: 'photo.jpg', data: bytes, type: 'image/jpeg' }] });
+run.state; // 'queued' (run.job, run.location = /api/v1/jobs/job_…)
+const done = await run.wait({ onEvent: (e) => show(e.job.progress) });
+done.result.files[0]; // { name: 'photo.png', mime, size, sha256, url, … }
+const bytesOut = await (await tools.jobs.file(done.job.id, 0)).arrayBuffer();
+
+// One tool's output feeds the next, without a download:
+const webp = await (await tools.run('webp', {}, { files: [{ job_id: done.job.id, index: 0 }], waitMs: 10000 })).wait();
+
+try {
+ await tools.run('jsonminify', { text: '{' });
+} catch (err) {
+ if (isToolRunError(err)) console.log(err.code, err.status, err.detail); // the tool's own problem+json
+}
+```
+
+- **What `run()` resolves to.** An inline tool, or a job that finished within `waitMs`, gives `{ state: 'succeeded', tool, result, took_ms, job?, location? }`. `result` carries `data` (output kind json), `text` (kind text) or `files` (kind file or files: the job's result files). A job still queued or running gives `{ state: 'queued' | 'running', tool, job, location }`. Both have `idempotencyKey`, `replayed` and a non-enumerable `wait(opts)`. On a finished run `wait()` resolves to the run itself, so `await (await tools.run(…)).wait()` works for every tool. On a job, `wait()` takes `jobs.wait`'s options (`signal`, `onEvent`, `lastEventId`), and `tools.jobs.wait(job.id)` gives the raw job.
+- **Failures.** A run that finished `failed` or `cancelled` throws `ToolRunError`. It is an `OpenVibeError` with the tool's problem+json mapped: `code`, `status` (the problem's, e.g. 422 or 504, not the HTTP 200), `detail` and `errors`. It also carries `state`, `tool`, `job` and `run`. When `err.retryable`, `tools.jobs.retry(err.job.id)` runs the failed job again. A refusal before the tool ran is a plain `OpenVibeError`: 404 `tools.tool.not_found` or `tools.tool.not_runnable` (`api: false`: the YouTube downloader is page-only and never in the SDK), 422 `tools.input.invalid` (with `errors[]` pointers), 401 `token.*`, 403 `capability.denied`, 413/415 for files, 429 `quota.exceeded` or `tools.job.too_many_active`, 503 `tools.tool.unavailable`.
+- **Idempotency and retries.** Every run carries an `Idempotency-Key`, generated when you pass none and returned as `idempotencyKey`. The same key and request give the same job (`replayed: true`); a different request under that key is 409 `tools.job.idempotency_conflict`. Inline tools ignore the key. Because of the key, the client retries a 429 after its `Retry-After` (capped by `maxRetryDelayMs`) and retries 5xx and timeouts without creating a second job. To survive a crash between the run and saving the job id, derive the key from the request and run again with it.
+- **Files.** Uploads use the jobs client's shapes: a `Blob`/`File`, or `{ name, data: Buffer | Uint8Array | ArrayBuffer | Blob | string, type? }`. They go as multipart `file` parts. References are `{ media_id: 'med_…' }` (a Media object you may read) and `{ job_id: 'job_…', index }` (a result file of your own job). They go in the JSON body's `files`, or, beside uploads, as a multipart `files` part holding their JSON. The tool gets the uploads first, then the references in order, and the count must fit the descriptor's `files.min`/`files.max`.
+- **Waiting and timeouts.** `waitMs` (0 to 60000) lets a job tool answer finished when it is quick. Each attempt's timeout is raised to `waitMs`, or to the tool's `limits.timeoutMs` once this client has read its descriptor (`get` or `list`), plus 10 s. Pass `timeoutMs` to set it yourself. `signal` aborts the run (`sdk.aborted`), and a `wait()` too. Aborting a wait leaves the job running.
+- **Who may run what.** Callers are tiered anonymous < session < user < app/service. Without a token you run the tools whose descriptor says `auth.anonymous: true`, keyed by IP. A person's token runs every tool except network probes. An app or service token (audience `openvibe.tools`) needs the descriptor's `auth.capability`: `tools.tool.run` (public), or `tools.net.probe` (partner: staff grant it by hand). Quotas count each tool's `cost` within its `quotaClass`. Registry reads (`tools.tool.read`) are public and send no token unless you pass `anonymousReads: false`.
+- **Jobs.** `tools.jobs` is `createJobsClient()` on the same origin and credentials: the gateway's `/api/v1/jobs` facade, which fronts every satellite. It adds `retry(id)`, which returns `{ job, replayed }` (a failed job as a new one, `retry_of`; asking again returns the same retry), and `reference(id, 'community:paste:p_123')` / `unreference(id, ref)`, which keep a succeeded result while something points at it (`expires_at: null`). Until the facade is deployed (Tools S6), use `createJobsClient(client, { baseUrl: 'https://img.openvibe.tools' })` (or `audio.`, `docs.`) for jobs.
+
### Testing your app
### Testing your app
```js
```js
@@ -240,6 +287,7 @@ const platform = createMockPlatform({
@@ -240,6 +287,7 @@ const platform = createMockPlatform({
mediaApps: { 'my-app': {} },
mediaApps: { 'my-app': {} },
users: [{ username: 'ana' }],
users: [{ username: 'ana' }],
jobs: true,
jobs: true,
+ tools: true,
});
});
const client = createClient({ fetch: platform.fetch, tokenProvider: createServiceTokenClient({ clientId: 'app_01K5WZX7S7Q4D2B8N3M6V1C9TR', clientSecret: 's', fetch: platform.fetch }) });
const client = createClient({ fetch: platform.fetch, tokenProvider: createServiceTokenClient({ clientId: 'app_01K5WZX7S7Q4D2B8N3M6V1C9TR', clientSecret: 's', fetch: platform.fetch }) });
```
```
@@ -251,9 +299,10 @@ The mock answers at the real public origins with real RS256 tokens and checks au
@@ -251,9 +299,10 @@ The mock answers at the real public origins with real RS256 tokens and checks au
- **Sandbox:** as in production, Media (on `/api/v1/<project_id>/files`) and Events (on the `events.app.*` routes) accept sandbox app tokens and keep their data apart from production; every other route and mock service refuses `env: sandbox` tokens (`401 token.sandbox_refused`) unless `acceptSandbox` lists the audience or capability. Unlike Network, sandbox apps get tokens for any audience unless you pass `sandboxAudiences`, and projects created through the API start with the whole catalog as allowance unless you pass `defaultAllowance: []`.
- **Sandbox:** as in production, Media (on `/api/v1/<project_id>/files`) and Events (on the `events.app.*` routes) accept sandbox app tokens and keep their data apart from production; every other route and mock service refuses `env: sandbox` tokens (`401 token.sandbox_refused`) unless `acceptSandbox` lists the audience or capability. Unlike Network, sandbox apps get tokens for any audience unless you pass `sandboxAudiences`, and projects created through the API start with the whole catalog as allowance unless you pass `defaultAllowance: []`.
- **Events:** publish with `event_id` dedupe, pull (with a `gap` after `pruneEvents(seq)`), checkpoints, subscriptions, `/realtime/stream` SSE with `Last-Event-ID` and gap events, and a delivery worker: `deliverEvents()` / `startDeliveries()` POST signed deliveries (v1 and v2 headers, fresh timestamp per attempt) to your local endpoint in order, retry, and mark them dead after `max_attempts`. Developer apps follow OpenVibe.Events' rules: app tokens are judged only on `events.app.publish | read | subscribe`; types `app.<project_key>.<name…>`, source `app-<ulid>`, actor the app or its `on_behalf_of` user; reads, checkpoints and subscriptions limited to the own project in the token's env plus public first-party events (every pattern starts with a literal segment, `app.*` patterns name the own key); app endpoints https and not loopback, private or local names; first-party readers never see sandbox events and see app events only through `app.*`; realtime streams neither. Not modelled: per-project quotas, revocation, and the DNS half of the endpoint check. Your delivery worker's `fetch` (`deliverEvents({ fetch })`) can route `https://hooks.example.com/…` to a local server.
- **Events:** publish with `event_id` dedupe, pull (with a `gap` after `pruneEvents(seq)`), checkpoints, subscriptions, `/realtime/stream` SSE with `Last-Event-ID` and gap events, and a delivery worker: `deliverEvents()` / `startDeliveries()` POST signed deliveries (v1 and v2 headers, fresh timestamp per attempt) to your local endpoint in order, retry, and mark them dead after `max_attempts`. Developer apps follow OpenVibe.Events' rules: app tokens are judged only on `events.app.publish | read | subscribe`; types `app.<project_key>.<name…>`, source `app-<ulid>`, actor the app or its `on_behalf_of` user; reads, checkpoints and subscriptions limited to the own project in the token's env plus public first-party events (every pattern starts with a literal segment, `app.*` patterns name the own key); app endpoints https and not loopback, private or local names; first-party readers never see sandbox events and see app events only through `app.*`; realtime streams neither. Not modelled: per-project quotas, revocation, and the DNS half of the endpoint check. Your delivery worker's `fetch` (`deliverEvents({ fetch })`) can route `https://hooks.example.com/…` to a local server.
- **Media:** the files API and `GET /f/:key`, with Media's tenant rules: `media.object.upload` uploads and deletes, `media.object.read` lists and gets (app keys, service tokens and app tokens alike); a developer app reaches only `/api/v1/<its project_id>/files`, where production uses the tenant `prj_…` and sandbox `prj_…-sandbox` (100 MB, `mediaQuotaMb`); sandbox files come back as `{ sandbox: true, url: <signed>, url_expires_at }` and `/f/<key>` serves them only with a valid signature.
- **Media:** the files API and `GET /f/:key`, with Media's tenant rules: `media.object.upload` uploads and deletes, `media.object.read` lists and gets (app keys, service tokens and app tokens alike); a developer app reaches only `/api/v1/<its project_id>/files`, where production uses the tenant `prj_…` and sandbox `prj_…-sandbox` (100 MB, `mediaQuotaMb`); sandbox files come back as `{ sandbox: true, url: <signed>, url_expires_at }` and `/f/<key>` serves them only with a valid signature.
-- **Tools jobs** (`jobs: true | { stepMs, handlers }`) at `origins.tools` and the satellites `img.`, `audio.` and `docs.openvibe.tools` (`platform.toolsOrigins`; `toolsSatellites` overrides), each with its own jobs: submit with Idempotency-Key replay, get, cancel, SSE with `Last-Event-ID` and `204` when finished, result files; `dropJobStreams()` simulates a dropped connection.
+- **Tools jobs** (`jobs: true | { stepMs, handlers }`) at `origins.tools` and the satellites `img.`, `audio.` and `docs.openvibe.tools` (`platform.toolsOrigins`; `toolsSatellites` overrides). Each satellite keeps its own jobs, and `origins.tools` is the gateway facade that also finds theirs. Covered: submit with Idempotency-Key replay, get, cancel, retry, references, SSE with `Last-Event-ID` and `204` when finished, and result files. Every view is `tools.job@1`, with `error` as problem+json. `dropJobStreams()` simulates a dropped connection.
+- **Tools platform API** (`tools: true | { descriptors, handlers, mediaObjects, stepMs }`) on `origins.tools`: `GET /api/v1/tools[/:id[/schema]]` and `POST /api/v1/tools/:id/run`, answering as openvibe-contracts v0.33.0 says. The default tools are `dns` (sync), `jsonminify` (a client tool with a server engine), `png` (a job), `port` (a probe needing `tools.net.probe`), `yt` (page-only) and `protectpdf` (unavailable). `descriptors` adds or replaces tools, and `handlers` answers them (`{ data }` or `{ text }` inline, a job handler for job tools). Also: `addTool()`, `addMediaObject()` (what `{ media_id }` reads) and `state.tools`. The mock checks caller tiers, the refusal codes, the file count and type, and the input's top-level fields. Idempotent job runs, `wait_ms` and file references work. It has no browser sessions, so job tools need a token. It has no quotas either: to test a 429, wrap `platform.fetch`.
-Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()`, `setAuthorization()`, `publishEvent(envelope, publisher, { projectId, env })` (a registered app's `app:<id>` publisher implies them), `pruneEvents()`, `deliverEvents()`, `dropRealtime()`, `dropJobStreams()`, and `stats` and `state` for assertions. It is a fake. It has no persistence, its visibility rules are simplified, and it has no Chat (no WebSocket mock).
+Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()`, `setAuthorization()`, `publishEvent(envelope, publisher, { projectId, env })` (a registered app's `app:<id>` publisher implies them), `pruneEvents()`, `deliverEvents()`, `dropRealtime()`, `dropJobStreams()`, `addTool()`, `addMediaObject()`, and `stats` and `state` for assertions. Its `fetch` rejects on an aborted signal, as `fetch` does. It is a fake. It has no persistence, its visibility rules are simplified, and it has no Chat (no WebSocket mock).
### Browser without a bundler
### Browser without a bundler
@@ -264,12 +313,12 @@ Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()
@@ -264,12 +313,12 @@ Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()
</script>
</script>
```
```
-`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`, `jobs` 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`, `jobs`, `tools` and `projects` as namespaces. `test/bundle.test.js` fails when it is stale, and the browser secret scan covers it.
## 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.
-- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release. CI clones `OpenVibe.Contracts` at the pinned tag for this check.
+- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release, the `openvibe-contracts` devDependency (a release tarball pin, v0.33.0), which the tools tests also use to validate requests and answers. It is never a runtime or peer dependency.
- A new public capability ships as a minor release: first the contract, then the wrapper here, then the service's route. Removing a wrapper is a major release, after the capability's deprecation window in Contracts has passed.
- A new public capability ships as a minor release: first the contract, then the wrapper here, then the service's route. Removing a wrapper is a major release, after the capability's deprecation window in Contracts has passed.
## Not wrapped yet (intentionally)
## Not wrapped yet (intentionally)
@@ -279,7 +328,7 @@ Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()
@@ -279,7 +328,7 @@ Helpers: `signUserToken()`, `signServiceToken()`, `signAppToken()`, `authorize()
- **Media from the browser:** Media refuses user JWTs on `/api/v1/:app/files`, so a browser uploads through its own app server. That server holds the app key and names the user with `actingUserId`.
- **Media from the browser:** Media refuses user JWTs on `/api/v1/:app/files`, so a browser uploads through its own app server. That server holds the app key and names the user with `actingUserId`.
- **Network coins, notifications and legacy-map writes** are internal service-to-service routes with no public capability, so they are not wrapped. Staff-only paste routes are also internal: admin stats, bulk, censor and the AI pass.
- **Network coins, notifications and legacy-map writes** are internal service-to-service routes with no public capability, so they are not wrapped. Staff-only paste routes are also internal: admin stats, bulk, censor and the AI pass.
- **Chat, Live, Billing and Games** from the original charter come when those services publish their capabilities in Contracts. Chat has no app principal (`chat.message.send` is `first-party`), so there is no Chat client and no Chat mock.
- **Chat, Live, Billing and Games** from the original charter come when those services publish their capabilities in Contracts. Chat has no app principal (`chat.message.send` is `first-party`), so there is no Chat client and no Chat mock.
-- **Tools job types** are not wrapped one by one: `openvibe-sdk/jobs` submits any `type` with its `input` and files; each satellite (img, audio, docs) documents its own types.
+- **Tools** are not wrapped one by one: `openvibe-sdk/tools` runs any tool with `api: true` by id, and its descriptor (`tools.get(id)`) gives the input schema, files and limits. `openvibe-sdk/jobs` still submits any job `type` directly. The YouTube downloader is page-only (`api: false`), so it is not in the SDK.
- **Realtime over WebSocket and presence** don't exist in Events yet (ADR-005). SSE is the only transport.
- **Realtime over WebSocket and presence** don't exist in Events yet (ADR-005). SSE is the only transport.
## Development
## Development
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.5.0",
+ "version": "0.6.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",
"updated": "2026-09-23"
"updated": "2026-09-23"
}
}
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.5.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.6.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, registry, modules, realtime, media, community, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -37,7 +37,7 @@ const { paginate, offsetPager } = __load(7);
@@ -37,7 +37,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.5.0';
+const SDK_VERSION = '0.6.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.5.0",
+ "version": "0.6.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.5.0",
+ "version": "0.6.0",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"better-sqlite3": "^11.10.0",
"better-sqlite3": "^11.10.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.5.0",
+ "version": "0.6.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Tools (registry, run API and jobs) and developer projects, 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, Tools (registry, run API and jobs) and developer projects, 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",
@@ -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.5.0';
+const SDK_VERSION = '0.6.0';
module.exports = {
module.exports = {
createClient,
createClient,