# Changelog ## 0.166.0 — 2026-11-13 **`scripts/test-services.sh` without Docker: the same services from local binaries** (CI checks on shared nodes). A check job on a shared node runs a repository's GitHub Actions job with act in host mode, with no Docker daemon and no root, so `eval "$(node_modules/openvibe-sdk/scripts/test-services.sh up)" && npm test && npm run test:pg`, the test line of nearly every OpenVibers repository, could not start its containers there. - With no working Docker (`docker info` does not answer), `up` starts PostgreSQL 18 (initdb, SCRAM, fsync off), PgBouncer (transaction mode; each test role is looked up with `auth_query` and SCRAM is passed through) and Valkey from local binaries, as the current user, under one throwaway directory (`OV_TEST_SERVICES_DIR`, default `$TMPDIR/ovsdk-services-`), on the same ports with the same users, and prints the same three variables. A second `up` reuses what runs; `down` stops them and removes the directory (and the containers, when Docker is there). - `OV_TEST_SERVICES=docker|local` chooses; `OV_TEST_SERVICES_PATH` limits where the binaries are looked for. Ubuntu: `apt install postgresql-18 pgbouncer valkey-server` (their own services can stay off). - Verified on ov-node-01 as an unprivileged user with no Docker: `testing-db`, `db`, `pg-outbox`, `db-ambient`, `valkey` and `govern` all ran against the local services (no case skipped) and `down` left nothing behind. Tests: `test-services` (no Docker and no binaries says what is missing and starts nothing; `OV_TEST_SERVICES` overrides). ## 0.165.0 — 2026-11-13 **`openvibe-sdk/agents/soul`: who an agent is** (agents.soul@1, Contracts 0.424.0; Design rules 175 `actor-is-the-work`, 176 `souls-grow-by-yes`). The owner, 11-13: "you should have different bots … they each have a soul … your agents are created based on your memories and personality". Pure functions: - `validate(soul)`: the shape and the rules a schema cannot say (no secret in a soul; no permission in a soul, which is the blueprint's hands and rules). - `toPrompt(soul, { name })`: one stable block of plain words (the dials in words, habits, beliefs, pushback, never, notes), placed before memory so a provider's cached prefix never changes inside a conversation; it says the soul never widens what the agent may do. - `toSoulMd`, `toIdentityMd`, `fromSoulMd`: the SOUL.md and IDENTITY.md layout OpenClaw and Hermes read, out and back; a SOUL.md from elsewhere has its sections mapped (Core Truths, Boundaries, Vibe…) and the rest kept as notes. - `change(soul, patch)`: an agent's suggestion for its own soul, as a proposed soul and the change in words; nothing is applied (its person says yes). - Tests: `agents-soul`. ## 0.164.0 — 2026-11-12 - **New `openvibe-sdk/ai-meter`:** every product meters its AI calls the same way (ai-metrics.md; Contracts 0.421.0 `ai.call.recorded@1` 1.1.0 and `ai.call-report@1`). - `startCall()` measures each call on its own clock: TTFT and latency less any queue, tokens per second after the first token (or Ollama's own decode time), tool calls, fallbacks, the error class, and the charged price when the provider gives one. - `normalize()` reads the usage of OpenAI chat completions and Responses, Anthropic Messages, Gemini and Ollama. The input it reports is the uncached input, with cache reads and writes and reasoning tokens apart. - `createMeterClient()` batches the records to AI's `POST /api/v1/usage/calls` with the service token. - It retries a network error, a 408, 425, 429 or 5xx with backoff, and asks for a new token after a 401. - A batch refused for any other reason is dropped and counted, never sent again. - AI prices any call that brings no price and publishes it. ## 0.163.0 — 2026-11-12 From reading 24 open agent projects (one-system-v2.md). Three new modules, all pure functions: - **`openvibe-sdk/agents/rules`:** permissions as data, from Gemini CLI and OpenCode (agents.permission-rule@1). - `decide()` lets the last matching rule win; with no rule, the run's level decides. - Unattended work never waits on an ask. It settles to allow at the levels approve-for-me and full, and to deny below them. - The floor always asks; with nobody there, it is denied. - `fromBlueprint()` compiles a blueprint to rules: only the tools it names, never `.git/hooks` or its own config, and its sandbox and network. - **`openvibe-sdk/agents/router`:** pays for the strong model only when it is likely to matter, in RouteLLM's shape over OpenVibe Decisions. - It asks for a calibrated probability that the small model is not enough, and routes against a threshold calibrated to a budget (the share of steps that should go strong). - It draws the quality-cost points the scoreboards show. - The strategy chain is the person's pick, then the blueprint's pin, then the router. When Decisions is down, the cheaper model runs. - **`openvibe-sdk/agents/metrics`:** what a run measured (agents.run@1 1.4.0). - The charged cost comes first; an estimate counts only where nothing was charged, and unknown is null, never 0. - It records tokens with cache reads and writes, steps, latency, a named exit and evidence. - Also `passAtK` (unbiased), `passHatK` and `flakeRate`. ## 0.162.0 — 2026-11-12 - **New `openvibe-sdk/agents/blueprint`: make your own agent** (agent-blueprints.md; Contracts 0.419.0's `agents.blueprint@1` and `agents.install@1`). These are pure functions, so a browser, Link, a CLI and every service check and run a blueprint the same way: - `validate`: the shape a person edits, plus the rules a schema cannot say. It carries no secret (anything that looks like a key or a token is refused where it is). Every `{{input}}` is declared. Level `full` has a budget. Machine and robot tools have somewhere to run. A shared place says what it needs, and never names a machine. A tool served over MCP has its shapes. A team never contains itself. - `withDefaults`: it talks only, asks first, keeps nothing, answers its owner, and spends 25¢ a run and $2 a day. - `asks`: the install screen in plain words, with the keys an install records as agreed. `unagreed` lists what an install has not agreed to yet. - `wider`: what a new version asks for that the old one did not. That includes a tool going from ask to allow, a higher level, a bigger budget, more people who may ask, more memory, a new place, trigger or team member. An empty list means an install set to "same-or-less" takes the version without asking. - `fill`: inputs, typed and checked. - `compile`: one run. It returns `agents.run-start@1`, the loop's system prompt (with the floor said in words) and tools, the Dispatcher's need, pin, rungs and price cap, the places and the install's machines, memory, and the team. Try it, or a run without an install, asks before every tool and spends at most 5¢. - `estimate`: the price of a run on the cheapest choice it may use, through `agents/dispatch`, and the rungs on which it is free. ## 0.161.0 — 2026-11-12 - **`openvibe-sdk/cam`: `capture(options)`** opens a camera through the site's own shared capture core (Shared's `ov-capture.js`, OVCapture). - It loads the core once from the calling site's own `/shared/` (never another origin) and resolves to a MediaStream. A page that already has OVCapture uses it. - The options are OVCapture's own: mode, facing, lens, mic, resolution, audio and video. - **Cam's in-page kit** (`CAM_KIT_SRC` / `CAM_KIT_SRI`) is re-pinned to the kit Cam serves today, checked against the live file at its versioned address. ## 0.160.0 — 2026-11-12 **The agent engine's loop, Dispatcher and Run API kit: `openvibe-sdk/agents/loop`, `/dispatch` and `/api`** (agent-engine.md §§3, 9). A model, tools and the person's words become a run's thread. It runs chat and ask turns on a server with no workspace (Vibe answering, an agent using OpenVibe's own products as tools). - **`runLoop(options)`** resolves `{ messages, stop, error, usage, cost, turns, toolCalls }`. Stop is `done`, `terminated`, `budget`, `turns`, `aborted` or `error`. - **The model adapter:** `complete({ messages, tools, system, signal, budgetLeft, onDelta })` returns the next turn: text, thinking, tool calls, why it stopped, usage and cost. `onDelta` streams the turn as it is written. - **Tools** run in parallel, or in sequence when the loop or a tool asks. Parallel runs are prepared in order, run together, and their results kept in the model's order. - **`beforeToolCall`** is the approval gate (block, with a reason, or end the run); **`afterToolCall`** may rewrite a result. - **`steering()`:** words the person sent while it works, taken before the next turn. **`followUp()`:** words after it would stop. **`finishTurn()`** may end or continue the loop. **`transformContext()`** compacts or filters the thread before each turn, and entries marked `ui_only` never reach the model. - **The budget** (usd, tokens, turns, steps) stops it between turns, never inside one; the model is told what is left. - **What it keeps, whatever the hooks say:** - A call cut off by the output limit never runs; the model is asked to send it again. - An unknown tool, input its schema refuses, a throw or a timeout comes back to the model as an error, never as a crash. - The same call repeated past `repeatLimit` gets a reminder instead of a run, and an idempotent tool answers the same call from the run's memo. - A long output is clipped before the model sees it; the whole result stays in the thread. - **`createStepSink({ append, update, delta, startSeq, startTurn, runId })`** turns the loop's events into `agents.step@1` steps. In order: the person's words, the thinking (folded), one tool step updated when it ends, then the agent's words streamed into their step. A person's message opens the next `turn` (Contracts 0.417.0). - **`checkInput(schema, value)`:** the JSON Schema subset tools use. - **New `openvibe-sdk/agents/dispatch`: the Dispatcher** (agent-engine.md §3), as pure functions over a person's choices: - `ladder(choices, { need, pin, avoid, budgetLeft })` orders them: their own machine, a signed-in plan, their own key, the free allowance, then credits, cheapest first inside a rung, and a smaller model before a stronger one unless the step needs strength. What cannot do the step (a capability, the context, the speed, the budget) is listed with why. A person's pick wins when it can, and is otherwise passed over with the reason. - `escalate(current, evidence, choices)` moves to a stronger model only on checkable evidence: a failed test, schema, tool or grader, or the model's own "not sure". A grader's failure also changes family. - `harnessFor(task, harnesses, { plans })` chooses by capability: the loop for turns with tools, the person's signed-in plan, else dsh. - `placeFor(run, { machines, cloud })`: the person's computer first (free), a Codes runner for a repository elsewhere, an OpenVibe computer when allowed, Bot for a robot. - `estimate(choice, tokens)` and `slice(parentBudget, share)`. - **New `openvibe-sdk/agents/api`: the Run API from a product's own store** (server). - `createRunApi({ product, subject, store, prefix })` returns `handle(req, res, next)` for Express or any Node http server: the list, start, get, steps, events, messages, stop and approvals routes of Contracts' `docs/agents-runs.md`. - The store has `list, get, steps, start, say, stop, answer` and, optionally, `follow` to push its own events (otherwise the stream reads new steps). A method a store lacks answers 409. - No subject is 401, another person's run 404, and a start without words or with an unknown kind 422. - Every run carries its ref. A store's own `{ status, code }` errors go out as they are; anything else is a 500 that says nothing of the store. A mandate from a caller is dropped (intake's hook sets it). - Read back with the SDK's own client in its test. - Ported from pi's agent loop (earendil-works/pi, `packages/agent/src/agent-loop.ts`, MIT, Copyright (c) 2025 Mario Zechner): the inner loop over tools and steering, the outer loop over follow-ups, the turn hook, parallel and sequential tools, and the two tool hooks. The model API, the message shape, the budget, the memo, the repeat guard, the timeouts and the step sink are OpenVibe's. ## 0.159.0 — 2026-11-12 The agent engine's client (Contracts 0.414.0: everything a person asks an AI to do is a run with a thread of steps). - **`openvibe-sdk/agents` runs:** `runs.list({ kind, status, q, limit, cursor, before, agent, parentRun })`, `runs.get(run)` (null on 404), `runs.steps(run, { after, limit })`, `runs.start(text | agents.run-start@1, { idempotencyKey })`, `runs.say(run, text | { text, attachments })` (continue a conversation or steer work), `runs.stop(run, { reason })`, `runs.events(run, { after })` and `runs.follow(run, { onRun, onStep, onDelta, onApproval, onEvent })`, which resolves with the end status. - **`approvals.answer(run, approval, answer)`** by the approval's id, its object, or the step seq that asked. - **`createFrameAgentsClient(client)`:** every product's runs through the Frame (`/api/frame/runs…`), where a run is named by its ref (`:`); `createAgentsClient(client, { prefix })` for any product's Run API. - **Helpers:** `refOf`, `parseRef`, `isEnd`, `needsPerson` (a run waiting for a message is a conversation between turns, not something that needs its person), `KINDS`, `STATUSES`, `END_STATUSES`. - **The stream:** the engine's events (`run`, `step`, `delta`, `approval`, `end`) are read beside AG-UI's; only a step advances the resume cursor, so a snapshot or more text that shares a step's id is never skipped. - **The plugin interfaces** (model adapter, tool, sandbox, memory, planner, reviewer) stay as JSDoc types; the loop that calls them is the next release. - `run`, `post`, `events`, `desks`, `takeOver`, `handBack` and `replay` are unchanged. ## 0.158.0 — 2026-11-12 - **`openvibe-sdk/views`: one view API for every content kind** (Contracts `media.view-request@1`, the `media.viewed` and `media.engaged` events). - `createViewsClient(client).record(event | [events])` posts a preview, watch or live heartbeat for `ref: { service, kind, id }` to Media's `POST /api/v1/views`, up to 20 at once. `seconds` is cumulative for the session and item, so a retry does not count twice; a signed-out viewer keeps an opaque session id. - `.counts(ref)` reads one item's `{ views, engaged, previews, preview_share, peak_concurrent }` from `GET /api/v1/views/counts`. Media never stores an IP address or a user agent with a view, and a preview counts from 1 second. ## 0.157.0 — 2026-11-12 See it work: a run's desks, control and replay - Add to `openvibe-sdk/agents`: `desks(run)` → the run's desks with their view and still, `takeOver(run, desk)`, `handBack(run, desk, note)` and `replay(run)` → the steps with the still each saw. They call the same-named routes under the client's service (`/api/v1/runs/:id/desks/...`), so a page and a script make the same calls. No tag: the main session releases this after the sites' stage lands. ## 0.156.0 — 2026-11-12 - **`openvibe-sdk/exchange`: reviewed compute prices, keyless.** - `createExchangeClient(…).computePrices({ kind })` and the standalone `createComputePriceClient()` read `GET https://openvibe.exchange/api/compute/prices`: each GPU or computer's hourly and per-second price, the provider's own price page, the date it was checked and the date it must be reviewed again, filtered to `kind: 'cpu' | 'gpu'` when asked. - The public endpoint needs no key, sends `Access-Control-Allow-Origin: *` and is cached for 60 seconds. The keyed client still speaks the API host (unchanged); its price read goes to `siteUrl` (default `https://openvibe.exchange`) without its developer key. ## 0.155.0 — 2026-11-12 - Add `design.icons.suggest(label, { context, limit, rank })` to `openvibe-sdk/design`: the best glyphs for a button's words by meaning (the glyph's name, its META keywords and its other-set names), with `rank: true` asking a small model to order the shortlist when the words are new. The answer is the same shape as `GET /api/v1/glyphs/suggest`, including `new`, `ranked`, `model` and, when known, `draft_price`. - `design.icons.get()` and `.search()` are public now, like the Design API they call: no access token. `.draft()` still needs one, and the client still sends a token when it has one. ## 0.154.0 — 2026-11-12 - Add the Frame's data client to `openvibe-sdk/frame` (and `sdk.frame` in the root, browser and ESM entries): `createFrameClient(client)` gives `choices(task)`, `query(q, { scopes })`, `providers()`, `ask(body)` and `events(run)`, the same calls every site's composer makes through OpenVibe.Network. `choices` answers the Model sheet (OpenVibe's models enriched from Exchange, the person's keys, computers and plans); `ask` starts a question and `events` reuses `agents.events` for its AG-UI stream, whose `ANSWER` event carries the numbered citations. Types are in `types/frame.d.ts`. The shared navbar, footer and `online()` are unchanged. ## 0.153.0 — 2026-11-11 - Add `objects.rotate(id, { degrees })` to `openvibe-sdk/media` (the v2 object client): turn a stored JPEG, PNG or WebP picture clockwise by 90, 180 or 270 degrees. Media keeps the uploader's original beside the turned copy, every generated size follows, and only the picture's owner may turn it (`subject` / `actingUserId` on a service client). ## 0.152.0 — 2026-11-11 A train of two pull requests: `openvibe-sdk/elements` (#227) and `openvibe-sdk/ui-layout` (#226). - Reserved for `openvibe-sdk/ui-layout` in OpenVibers/OpenVibe.SDK#226. That module is not released by this entry; it keeps the changelog's minor sequence while that pull request is open, and its own entry replaces this one at merge. (#227) - Add `openvibe-sdk/ui-layout` (also `sdk.uiLayout` in both entries): `validate(catalog, layout)` is the one check the server, the editor and the AI answer use, returning `{ ok, reasons[] }` in plain words. It refuses a missing, hidden or undersized required part, anything pressed under 44 px (and any part under the catalog's own pixel minimum, e.g. Stop at 64), a part covering a required part or breaking a `never_covers` rule, a tint that is not a catalog theme token or a style/binding the part does not allow, a part the catalog does not have, and any URL, markup or code. `applyDefaults(catalog, device?)` builds the valid default arrangement (required parts first, every part visible, 44 px met) and `merge(layout, patch)` applies a patch by part id without mutating the layout. Browser-safe, no dependencies; exhaustive tests. (#226) ## 0.151.0 — 2026-11-11 - Add related-origin passkey sign-in to `openvibe-sdk/sso` (opt-in `relatedPasskeys: true`): `POST /auth/passkey/options` and `POST /auth/passkey/verify` broker a WebAuthn ceremony for this site's exact origin through OpenVibe.Network as its OAuth client. The random browser binding stays in the site's own `__Host-` cookie; Network verifies the exact origin and RP ID and returns a single-use result that becomes the site's ordinary session. Browsers without `PublicKeyCredential.getClientCapabilities().relatedOrigins` keep the existing OAuth/passkey path. ## 0.150.0 — 2026-11-11 - Add `ai.turn(input, options)` for file-capable `ai.complete` runs. Its typed `output_file` option stores a text, Markdown, CSV or JSON answer in Media and returns private `ai.file` artifact references; existing `ai.files.read()` reads a returned object. - Type the image task's owner daily cap and guest find switch in `AiTaskDefaults`. ## 0.149.0 — 2026-11-11 - Extend the existing `codes` client with `modelPlans.get/put/presets()` and `tasks.create({ plan | preset })`, typed for `codes.model-plan@1` on Contracts 0.406.0. ## 0.148.0 — 2026-11-11 - #208: add `openvibe-sdk/composer`, the shared composer's client: `attachments.upload(file, { onProgress, signal })` (multipart through `XMLHttpRequest`, real browser progress), `attachments.remove(id)`, `composer.prefs.get()/put(prefs, rev)` and `mentions.search(q, kinds)`, typed by `composer.attachment@1`, `composer.prefs@1` and `composer.mention@1`. - #210: add `feed.list({ kind, cursor })` for the cross-site Community Feed, keep the existing Community reader callable, and type preview video media. - #211: add the `openvibe-sdk/sso` one-time cross-site sign-in handoff: `POST /auth/handoff` mints a 60 s ticket and `sso.handoff()` arrival middleware redeems it before the page renders. - #212: add `agents` SDK methods to fetch run snapshots, follow resumable AG-UI event streams and post approvals. - #213: add `wallet.changes()` for the owner-scoped Vibes, OpenCoins and channel-points gain and loss window, and `wallet.markRead()` for each currency's own read cursor. - #214: add browser-safe `player.current()`, `player.handoff(url)` and `player.on(listener)` for a live stream carried between OpenVibe sites by Shared's floating player. - #215: add typed Chat readers for a person's compacted day, week and month memory, plus personal erase and opt-out. - #216: add `exchange.createCatalogSearchClient().search(q, filters)` for public model and provider offers (prices, measured speed and Bench quality when available), and type the image search filters and source metadata. - #217: add the owner-only agent sheet reader and Actor pause, resume and memory controls through Network's relay. - #219: the app sign-in test checks a token's shape, not the letters "eyJ" (a macOS flake). - #220: `homeFeed.network.popular({ limit })` (the signed-out front door's five things people do now) and `homeFeed.network.tryNext({ limit })` (the signed-in person's own suggestions). - #222: type the exact reply target and its display name on a Community comment; a thread's first page includes two replies and the rest use the parent cursor. - #207: add `codes.tasks` for the Dock's Code tab: `list({ limit })`, `create({ text, mode, repo?, attachments? })` and `stop(id)` (`codes.task-summary@1`). - openvibe-contracts pinned at v0.406.0. ## 0.147.0 — 2026-11-10 - #181: add `chat.activity` (the person's feed: mentions, replies, reactions, server invites, accepted friend requests and roles), `rooms.react()` and `reply_to_id` on room and DM sends. - #182: add the `system` and `can_run` machine metadata to the machine reader type. - #201: add `quest.cardData({ token, timeZone })` for the shared quest card: today's quest, the public catalog kept ten minutes and the person's badges and quest log. - #202: add `chat.embeds(message)` and `readEmbeds()` for the typed `chat.embed@1` address previews on a chat message. - #204: add `tools.run(tool, input, { prefer })` for worker placement (`auto`, `home`, `host`) over JSON and multipart requests, with execution receipt types and `tools.jobs.get(id)` examples. - #205: add the browser-safe, typed `userCard.createUserCardClient(core).get(username)` public Network aggregate; hidden or unavailable fields stay optional and the read sends no credentials. ## 0.146.0 — 2026-11-10 - #183: add the private history reader for cursor reads, search, summaries, agent recent reads, settings and owner-scoped deletes. - #184: add `frame.online()` with member presence, anonymous guest counts and live channel/site items. - #186: add `quest.today()` for the authenticated daily Quest response. - #195: add `bot.embedUrl(robotId, mode)` for panel, overlay and compact embeds. - #196: add merged code search, cited deep answers and cursor iteration to Search. - #197: add filtered, paged Inventory holdings reads and counts, plus async holding pages. ## 0.145.0 — 2026-11-10 - `bench.ratings.get`: shared fitted ratings and confidence intervals with a twenty-game floor. Browser-safe, anonymous and typed; pairs with `ai.metrics` across sites. ## 0.144.0 — 2026-11-10 - `ai.metrics`: public daily model series, up-to-six-model comparisons, task leaderboards, listed price history and hourly provider health. Typed sample counts and nullable measurements; available in browser and server clients without credentials. ## 0.143.0 (2026-11-10) - **`sso`: a plain sign-in is quiet first.** `GET /auth/login` with no `silent`, `prompt` or `login_hint` now asks the Network with `prompt=none` before it shows a page: a browser signed in at openvibe.network comes back signed in, with no chooser. Before, a page that needs a person (and every link to one from another site, such as "Open" in the Dock) landed on the chooser although the Network knew the browser. When the Network knows nobody (`login_required`), or always asks for this app (`interaction_required`), the callback goes on to the chooser by itself (`/auth/login?ask=1`, the new way to ask for the chooser outright). A person who signed out on this site (`ov_sso_hint=guest`) gets the chooser at once, as do "switch" and "add an account" (`prompt`, `login_hint`). The navbar's own silent sign-in (`?silent=1`) is unchanged: with nobody at the Network it returns to the page with `?sso=none`. ## 0.142.0 (2026-11-10) - **`chat.rooms.memory(id)` and the moderator/person writes**: what a room remembers between summaries (`GET /api/v1/rooms/:id/memory`). `memory()` answers `{ room, entries, kinds, can_edit, me }`, each entry with its `kind`, `text`, `confidence`, when it was learned and its `sources` (the message it came from). `addMemory(id, { text, kind?, confidence?, source_message_ids? })` and `correctMemory(id, entryId, patch)` are a moderator's; `removeMemory(id, entryId)` removes any for a moderator and one learned from the caller's own messages for anyone; `forgetMyMemory(id)` forgets everything learned from the caller's messages in that room. Typed as `RoomMemory`, `MemoryEntry` and `MemoryInput` in `types/chat.d.ts`. ## 0.141.0 — 2026-11-10 - `community.comments.upload(ref, file)` uploads a Community paste comment attachment through Community; `comments.create()` accepts typed attachment references. ## 0.140.0 — 2026-11-10 - `chat.history.pages()` asynchronously iterates newest-first cursor pages; `chat.history.attach()` loads older pages on scroll while preserving the visible message anchor. The same helper is available to Live and Dock. ## 0.139.0 (2026-11-10) - `community.comments`: shared comment thread resolve/read/write/edit/delete and visibility methods, including ordered pages and paged replies. ## 0.138.0 (2026-11-10) - **`chat.rooms.insight(id)`**: a room's public AI insight (`chat.room-insight-result@1`), as `GET /api/v1/rooms/:id/insight` answers it: `{ overview: { text, topics, people, messages, from, to, updated_at }, timeline: [{ at, kind, title, text, message_id }], state: 'ready' | 'quiet' | 'off' }`. `id` is a room slug or `global`; a room the rollout does not cover answers `null` (404 like `rooms.get`). No account is needed, so an anonymous client reads it too. Typed as `RoomInsight` in `types/chat.d.ts`. ## 0.137.0 (2026-11-10) - Add `moneyReasons` (`billing.money-reason@1`): the plain words for a movement of OpenCoins, Vibes or Balance, for a service that holds the raw `reason` row. It re-exports `openvibe-contracts`' list when the installed package has it, otherwise the same list copied at Contracts 0.392.0: `describe(key, { site, resolve, subject })` answers `{ key, text, subject: { name, url } | null, site, known }`, and `describeRow(row)` reads `{ reason, app_id }`. An undeclared key reads "for something on " with `known: false`. ## 0.136.0 — 2026-11-09 - Network levels client: public person, batch and board reads, and service awards. ## 0.135.0 (2026-11-10) - `ov.ai.files`: attach files through Media, list/read/remove them, and expose typed `ai.file@1` and conversational turn shapes. ## 0.134.0 (2026-11-09) - `ov.ai.creations()`: the public catalogue of everything AI can make on OpenVibe (`GET /api/v1/creations`, `ai.creation-list@1`) — id, product, a plain name, what you give it and what comes back, the workflow or registered task behind it, who may use it (guest, signed-in, 18+ floor), whether it can cost coins, and where to start. Cached five minutes and CORS open; no token is sent. Types: `AiCreation`, `AiCreationList`. ## 0.133.0 (2026-11-09) - ov.blog.changelog({ service, kind, area, q, limit, before }): the network changelog's blog.changelog-entry@2 rows, newest first ## 0.132.0 (2026-11-09) - `openvibe-sdk/wiki`: articles on blocks (`articles.create`/`update({ blocks })`, `pages.blocks`), picture suggestions from the `wiki.article.images` task (`images.suggest`), and the `content.wiki` writer (`articles.draft`), whose revision is a pending proposal a person reviews. ## 0.131.0 (2026-11-09) - openvibe-sdk/community: the Feed's types complete, with pagination guidance ## 0.130.0 (2026-11-09) - openvibe-sdk/meme: ov.meme.templates(), .render() (PNG, GIF or MP4) and .get() ## 0.129.0 — 2026-11-09 - `openvibe-sdk/git` (G2): changes and review — `changes.list/get/create/comment/approve/merge/close/summary`, `issues.list/get/create/labels/close`, `compare()`, `blame()`, `search()` and `reviewModel()`. Review requests carry the expected head SHA, merge methods are checked, and mutations are POSTs with retries disabled. ## 0.128.0 (2026-11-09) - ov.ai: tasks(), choices.get/set, run(task, input, {choice}) and admin.defaults(); runs.create accepts `task` and `choice` ## 0.127.0 — 2026-11-09 - `openvibe-sdk/git` (`ov.git`): repositories, trees, blobs, commits, branches, tags, releases, contributors, insights and stars; `commits.create()` writes a branch and files without a clone; `tokens.create()` makes a short-lived clone token; `clone(repo, { to: 'link' })` hands a clone to Link. ## 0.126.0 (2026-11-09) - Add `openvibe-sdk/pics`: the public gallery and picture reads, private uploads with resumable chunks, publication, likes and albums on OpenVibe.Pics. ## 0.125.0 (2026-11-09) - openvibe-sdk/blog: posts are written and read as Markdown or as blocks (publishing.block@1), ?format=blocks ## 0.124.0 (2026-11-09) - OpenVibe Link: `openvibe-sdk/node` is `openvibe-sdk/link` (`ov.node` is `ov.link`), building `platform.link-capabilities@1` and mapping a Link to its `platform.resource-offer@1` and `platform.runtime-offer@1`. A link principal token is never a user token. The Contracts pin moves to v0.372.0. ## 0.123.0 (2026-11-09) - ov.exchange: keys (list, create, revoke), private usage, deployments, and web, image and icon search with fetch (the Exchange API platform) ## 0.122.0 (2026-11-09) - dms.viewPreference() and setViewPreference(): a conversation's saved view, bubbles or room ## 0.121.0 (2026-11-09) - ov.cam.settings.get/put: a person's saved camera choices (Cam #116) ## 0.120.0 (2026-11-09) - createUploadClient(): resumable Media uploads in 8 MiB parts, three at a time, with a session reference to resume; ov.ui.progress(el) ## 0.119.0 (2026-11-09) - openvibe-sdk/wallet: the wallet, Vibes, the store and items, through Billing and Inventory with the person's bearer and idempotency keys ## 0.118.0 (2026-11-09) - openvibe-sdk/design: `components()` and `component(name)` read OpenVibe.Design's public design.component@1 registry (a missing name is null, a bad one is refused before a request). They are on the same `createDesignClient` as the icons; `accessToken` is now optional and only the icon API needs it. ## 0.117.0 (2026-11-09) - ov.cam loads Cam's kit at its versioned URL and its new hash (Cam #116 changed the kit) ## 0.116.0 (2026-11-09) - npm metadata (keywords, homepage, funding), the README's first screen and CITATION.cff ## 0.115.0 (2026-11-09) - Tools discovery: ov.tools.popular() and ov.tools.trending() ## 0.114.0 (2026-11-09) - Dock v2: Vibe conversations (ov.chat.vibe.conversations) and Actor skill runs (ov.actor.skills.run) ## 0.113.0 — 2026-11-09 - Add the Feed and status posts to `openvibe-sdk/community`: `feed({ kind, site, q, following, cursor, limit })` reads Community's `GET /api/v1/feed` (one stream over Pulse from every OpenVibe site) and `posts.create({ text, pictures })` posts up to four Media pictures with required alt text, in the Feed at once. ## 0.112.0 — 2026-11-09 - Add the person's stats to `openvibe-sdk/report`: `report.me({ days })` (GET `/api/v1/me/stats`, the signed-in person's own numbers), `report.person(username)` (GET `/api/v1/u/:name/stats`, only the sections they published) and `report.publish({ sites, activity, made, ai, money }, { days })` (PUT `/api/v1/me/stats/published`, all five switches named). The answers are `report.me-stats@1`; a source that does not answer leaves its section out, never a zero. `me` and `publish` need the person's own session token; `person` is public and is a 404 when nothing is published. ## 0.111.0 — 2026-11-09 - **`openvibe-sdk/shoes`: the Open.Shoes catalogue and sizing tools.** - `createShoesClient(client)` (server-side; `./shoes` is browser-blocked) exposes public reads only: `products.list({ kind, q, size })`, `products.get(id)`, `products.offers(id)`, `tools.laceLength({...})` and `sizes.convert({ system, size, brand })`. ## 0.110.0 — 2026-11-09 - **`openvibe-sdk/design`: a typed client for the Design glyph API.** - `createDesignClient({ accessToken, baseUrl })` returns `icons.search(q)`, `icons.draft({ name, sentence, styles, sourceSvg })` and `icons.get(name, { style })` for the authenticated glyph draft, get and search calls. It is server-side only (the `./design` subpath is browser-blocked) and takes a signed-in access token rather than a service token. - Styles are checked against the set the API accepts (`thin`, `light`, `regular`, `bold`, `solid`, `duotone`). ## 0.109.0 — 2026-11-09 - Add Vibe compare methods to `openvibe-sdk/chat` for submitting answers and recording a user's preferred model. ## 0.108.0 — 2026-11-09 - Add daily quest reads, progress, step type listings and reviewable step contributions to `openvibe-sdk/quest`. ## 0.107.0 — 2026-11-09 - Add `ov.cam.open()` for the Cam recorder, with typed modes, options and file results across Node and browser exports. ## 0.106.0 (2026-11-09) - Add `openvibe-sdk/home-feed` and `homeFeed` to the top-level and browser exports, with home layout, Today frame, and personal feed methods. ## 0.105.0 (2026-11-09) - Add Report AI leaderboard, model, provider, comparison and picks reads, plus Exchange Arena start and vote methods. ## 0.104.0 (2026-11-08) - Add `createAccountEmailsClient` to `openvibe-sdk/identity` for managing the signed-in person's verified Network email addresses. ## 0.103.0 (2026-11-08) - Add `openvibe-sdk/credentials`: `createCredentialsClient` for a person's own provider credentials on OpenVibe.AI: `list()` (metadata only: kind, provider, label, created, last used, expired), `add()` for an API key or a browser session cookie, `remove()`, and `balance()` (that credential's fixed read-only balance readers: Anthropic Console prepaid credits and current spend, OpenAI credit grants). A secret never comes back from any call. ## 0.102.0 (2026-11-08) - Add `openvibe-sdk/trust`: `createTrustClient({ networkUrl, getToken })` with `.me()` (the caller's own signals, a person's or a guest's token) and `.of(subject)` (a service token with `identity.trust.read`), the typed `identity.trust@1` result for accounts and guests, and pure helpers (`isNew`, `atLeast`, the age-band order) for Node and the browser bundle. Signals are age bands and aggregate counts only: never an IP, email or last-seen time. ## 0.101.0 (2026-11-08) - Add bounded `locker/server.inspectEnvelopePrefix` validation against an actual stored ciphertext byte count. - Allow service-bound staged envelope receipts in share updates, with the existing recipient, nonce and atomic reseal checks. - Allow Locker public/private key export after `importPersonKeys`, so a recovered locker can upload and share using the same WebCrypto operations. ## 0.100.0 (2026-11-08) - Add `openvibe-sdk/compute`: shared compute strategies, hard placement constraints, RAM-aware pool scaling, exact billing arithmetic and provider quote/lifecycle adapters. See `docs/compute.md` for gates and unsupported purchase paths. ## 0.99.0 (2026-11-08) - **`openvibe-sdk/events`: `outboxSchema()` indexes the rejected rows `rejected()` counts.** Every outbox made with the helper now also gets `_rejected` on `(rejected_at) WHERE rejected_at IS NOT NULL`. Before it, the count read the whole outbox every time it was not cached (523 ms and 112 M buffers a day on Deals, 2026-11-08), because the product had no index on `rejected_at`. A service that already has its outbox table must add the same statement in an expand migration: the DDL is a migration helper, never run at boot, so `CREATE INDEX IF NOT EXISTS` in this helper only reaches tables created after it. ## 0.98.1 (2026-11-07) - `ai.batches.cancel` says what cancelling does: only the prompts that never had a run are cancelled, and nothing is charged for them. A prompt that already has a run (the provider is working on it) goes on and settles on its own path: it keeps its status, the answer says so, and nobody is told it was not charged. The batch itself is cancelled only when nothing is left in flight. The same note is on `AiBatchItem.reason`. No runtime change. ## 0.98.0 (2026-11-07) - `ai.runs.create` (and the direct operations) may ask for the class the caller may wait in: `{ latency: 'interactive' | 'background' | 'batch' }`, and for a batch run `{ deadlineHours: 1..72 }` (how long the answer may take). The request carries `options.latency` and `options.deadline_hours`. The class belongs to the workflow: a caller may ask for a slower class and never for a faster one, and a faster ask is clamped by the service, not refused — the run answers with the class it really ran in (`run.latency_class`), the tier it was priced on (`run.service_tier`) and, when it was clamped, `run.latency_clamped`. - `ai.runs.waitFor` learns that a batch run may take hours: when the wait is over and the run is a batch run, the pending run is returned (its status says where it stands) instead of an error. The answer comes later, with the run's completion event (`ai.run.succeeded`) or the webhook the run was made with. - Add `ai.batches`: `create(items, { model, deadlineHours, idempotencyKey })`, `get(id)`, `list()` and `cancel(id)` for OpenVibe.AI's `/api/v1/batches` — many prompts answered later, at half the retail price. A cancelled batch cancels the items that had not run; nothing is charged for them. - Add the types `LatencyClass`, `ServiceTier`, `AiBatch`, `AiBatchItem` and `AiBatches`, and `CLASSES` (the three classes) next to `TERMINAL`. ## 0.97.1 (2026-11-07) - `types/menu.d.ts`: `MenuRequest` gives `url` and `method` the `| undefined` that `node:http`'s `IncomingMessage` has, and `MenuResponse` no longer adds an index signature, so a plain `IncomingMessage`/`ServerResponse` handler type-checks under `exactOptionalPropertyTypes` (Games' `createMenuCardHandler`). No runtime change. ## 0.97.0 (2026-11-07) - Add `openvibe-sdk/menu`: `menuCard({ id, site, title, kind, items, more, origin, ttlSeconds, updatedAt })` builds one validated `frame.menu-card@1` with the house rules applied — plain text (markup and control characters stripped) cut to the contract's lengths, at most six items (the contract's cap), https links on the site's own origin or an `openvibe.*` host, and an optional item image (`{ src, w, h, alt }`) with the same rule. A bad field throws a `TypeError` naming it; an empty `items` array is valid and Network skips the card. - Add `menuRoute(getCards, { maxAgeSeconds = 300, origin, log })`: the Express/node:http handler for `GET /.well-known/openvibe-menu.json`. It calls `getCards()`, validates and caps the file at three cards, and answers `Cache-Control: public, max-age=…` with CORS `*`; a build failure or an invalid card answers `[]` (never a 500) and logs once. `MENU_PATH` is the route. - TypeScript types, README ("Put your content in the OpenVibe menu"), the API surface and the pack test follow; the devDependency moves to openvibe-contracts v0.324.0, whose `frame.menu-card@1` tests validate every card this module builds. ## 0.96.0 (2026-11-07) - `openvibe-sdk/actor` gains `contacts`: `list()`, `presets()`, `get()`, `create()` (optionally from a preset), `update()`, `remove()` and `setLevel()`, for a person's agents as contacts and their approval levels (plan §12 A-1). `contacts.list()` is `GET /api/v1/agents` with the person's own token, which now answers their contacts (Vibe first), so `agents.list()` and the new `agents.catalog()` call the explicit public catalog `GET /api/v1/agent-systems` and keep their old meaning. `tasks.create` types `agent_id` and `level`. ## 0.95.0 (2026-11-07) - Add `openvibe-sdk/cam`: `defineOvCamera({ src })` injects OpenVibe.Cam's framework-free `` 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 scheduled public agent streams; `steps.post()` and `postStep()` for any harness; `digest.read()` and `poll.open()` / `poll.result()` for a Co-pilot's service token; and `onControl()` to honour pause, curtain, mute and end by polling the stream's control log. The guards run before anything leaves the process (an `scd_…` id, a step's title and status, two to four poll options, a 10–300 second poll). - Add `openvibe-sdk/reports`: `write()` posts a session report with the person's own token (private by default, idempotent per source and task ref) and `read()` gets one the caller may see. The Report service redacts every text field; the client never sends a raw transcript. - Add `examples/cli-agent-stream.js`: about 40 lines that start a stream, spawn any CLI command, post its steps and end the stream with the command's result, using the same ticket path a Link desk uses. ## 0.93.0 (2026-11-06) - `openvibe-sdk/exchange`: the seller side gains the **endpoint form**, so any OpenAI-compatible server can sell, not only an OpenVibe Link machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with no `machineId` — registers the server on OpenVibe.Exchange's public seller API (`POST /api/v1/exchange/seller/offers`) with the same developer key the buyer side uses. The owner is the person the key acts for (a developer app's project owner), never a body field, and the SDK refuses an endpoint that is not https before anything is sent. - The platform reaches that address only through its egress guard (https, a public DNS answer checked inside the connection, never an IP literal, redirects judged again, a timeout and a response byte cap, no buyer identity in a header) and verifies it exactly as a machine running Link (canaries, the model fingerprint, about 1% spot re-runs, the free tier until verified, self-dealing earns 0). Its answers are labelled `community-endpoint:` on the receipt. - `seller.endpointOffers()` lists the caller's endpoint offers with their verification; `seller.removeEndpointOffer(id)` removes one. `machineId` still selects the Network registry form. - `seller.probe()` is unchanged, and `seller.offer` still probes the server's OpenAI model list first when `endpoint` is given (`probe: false` turns that off). ## 0.92.0 (2026-11-06) - Add `openvibe-sdk/chat-apps`: the three chat apps (Telegram, Discord, Slack) in one place, so a service that answers a chat app reads them the same way (the plan of 2026-10-30, stage A4: "your agent in Telegram, Discord and Slack"). - The checks: `verifyTelegram()` (the `X-Telegram-Bot-Api-Secret-Token` header, compared in constant time), `verifySlack()` (v0 over "v0::", the timestamp within five minutes), `verifyDiscord()` (Ed25519 over "" with the application's public key) and `verifyDelivery()` (the provider names its own scheme; a provider this does not know is never verified). The code came from OpenVibe.Watch, which has checked these deliveries since 2026-11-03 (Watch #194 and #195). - The buttons: `signCallback()` and `verifyCallback()` carry a whole signed payload with its expiry; `signButton()` and `verifyButton()` carry a 16-character row id in 44 bytes, inside Telegram's 64-byte `callback_data`, and the caller checks who may press it, what it does, that it is used once and that it has not expired against its own row. A token that was changed, replayed or is too old is refused. - The messages: `parseTelegramUpdate()`, `parseDiscordInteraction()`, `parseDiscordMessage()`, `parseSlackEvent()` and `parseSlackInteraction()` give each app's payload the same shape (who said it, where, what they said, whether it was said to the bot, and the button that was pressed), never the app's own JSON and never a message the bot itself sent. `splitText()` splits one answer the way the app accepts it, and `commandOf()` reads a command and its arguments. ## 0.91.0 (2026-11-06) - `openvibe-sdk/streams`: the Agent Streams plan, stage 1 — an agent's own stream on OpenVibe.Live (a stream of kind `agent`, id `ags_…`, no stream key and no slot, published with a ticket instead of a key). - `startAgentStream({ title, task, visibility, allowSites })` answers `{ stream, publish }`: the stream (live.agent-stream@1) and the publish part (live.agent-stream-publish@1) with the WHIP url, the publish ticket (shown once; Live keeps only its hash) and the moment it dies. - `renewTicket(id)` and `revokeTicket(id)`: the ticket lives four hours (the desk renews it at three) and revoking it is the kill switch — Live ends the WHIP session at once. - `state(id, { ticket })` is what the desk polls every ten seconds: `{ viewers, encode, curtain, ends_at }`. While `encode` is false the desk stops encoding and sends a snapshot every five seconds with `postFrame(id, bytes, { ticket, contentType })` (JPEG or WebP, at most 200 KB). - `setCurtain(id, up, reason)` raises the desk's privacy screen (while it is up Live pauses every viewer's consumer, so a viewer sees "Private screen" and never why), `setVisibility(id, v)` switches who may watch, `end(id)` ends the stream (the card stays, with the last frame and the VOD) and `viewTicket(id)` mints the player's view ticket (ten minutes). - The publish ticket is a bearer of its own: on the desk's routes (`state`, `postFrame`) the desk sends the ticket, never the person's token. Every body is checked in the module before it leaves, and a bad id, visibility, curtain reason or an oversized frame is refused with a TypeError. - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Link principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream. ## 0.90.0 (2026-11-06) - **Subject assertions** (ADR-003, the 2026-11-06 amendment; plan H1). A money call carries a Network-signed proof that the person allowed the calling service to act for them, beside the name of the person. - `requestSubjectAssertion({ userToken, audience, token | tokenClient, network | internalUrl })` asks Network to sign the person's own access token into a 60-second assertion for one audience (`openvibe.billing`). The person's token is the only thing that can produce one: without it Network signs nothing. - `verifySubjectAssertion(assertion, { jwks, audience, act, seen })` checks the ES256 signature against Network's JWKS (the EC keys, cached like the RS256 ones), the audience, the expiry and the jti; a forged, expired, wrong-audience or wrong-service assertion is refused with a stable code (`assertion.bad_signature`, `assertion.expired`, `assertion.wrong_audience`, `assertion.actor_mismatch`, …). - `createSubjectAssertionVerifier({ jwks, audience })` remembers the jtis it has seen, so a replayed assertion is refused inside the replay window. - `subjectAssertionHeader(assertion)` gives the `x-ov-subject-assertion` header for the call. ## 0.89.0 (2026-11-06) - Add `openvibe-sdk/exchange`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders", item 7). - The buyer side speaks the Exchange’s v1 API with a Network developer key: `chat()` (streaming and not, the OpenAI shape), `models()`, `offers(model)`, `catalog()`, `presets()` (list, create, update, delete), `usage()` and `receipt(id)`. Every call takes an `AbortSignal`, and a 429 or a 5xx is retried with a jittered backoff: a safe method always, a write only when it is idempotent, so a completion is never run twice. - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe Link’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime). - The webhooks (usage, earnings, an offer going down) carry `X-OpenVibe-Signature-V2`; `webhooks.verify` checks the raw body in constant time and refuses a timestamp more than 300 s either way. ## 0.88.0 (2026-11-06) - `openvibe-sdk/share` learns locked items (Locker). A share records how its item is sealed — `lock: none | people | password | link`, set by `register(ref, { lock })` and answered by GET — and `describe()` cannot override it. A locked item is never public (`share.lock_public`), and the lock cannot change after registration (`share.lock_fixed`). Expand-only migration: `shares.lock`, `share_members.key_slot`, `share_transfers.key_slot`. - A people-locked item carries each member's key slot (`locker.people-slot@1`). `addMembers`/`update` refuse a member without a slot (`share.slot_required`), refuse a slot for another subject or key, and answer `share.key_changed` (409) when the slot's `recipient_key` is not the RFC 7638 thumbprint of that person's key in Network's key directory now (`GET /api/v1/people/:subject/keys`, cached a minute). A person with no locker yet is `share.no_key`. - Removing somebody from a people-locked item is a re-seal: the save carries `envelope` (base64 of a people-locked `locker.envelope@1`), or `removeMember(..., { envelope })`. The envelope must not still hold the leaving member's slot (`share.reseal_invalid`), and it is stored through the new `onReseal(ref, envelopeBytes, tx)` hook inside the transaction, before the member row goes; a hook error rolls everything back. Without an envelope: `share.reseal_required`; without the hook: `share.reseal_unavailable`. A save that carries an envelope may be up to 8 MiB; every other body stays at 64 KiB. - A re-seal must be fresh (`share.reseal_not_fresh`): its nonce base may not repeat the one the module stored last, and a staying member's slot may not be byte for byte the stored one. Either means the envelope already stored was sent again, so the person leaving could still open what is served next. The last nonce base is kept in `shares.reseal_nonce_base` (expand-only). - A transfer of a people-locked item carries the recipient's `key_slot` with the offer (required, checked like a member slot); accepting drops the former owner's membership unless `keep_access`. - GET for a people-locked item answers each member's `key_slot`, and for the owner each member's `key_fingerprint` and `fingerprint_changed`, so the sheet can warn and re-wrap. - The agent client gains `addLocked('@name', { contentKey })` (looks up the person's key, wraps, saves the member and the slot) and `removeLocked('@name', { contentKey | envelope })` (re-seals for the owner and the members who stay, then removes); `share({ base, token, network })` names the key directory. `transfer(to, { key_slot })` passes the recipient's slot. - `openvibe-sdk/locker`: `open`/`rewrap` accept `{ contentKey }`, so an agent that sealed an item can open or re-seal it without a private key. `openvibe-sdk/locker/server` exports `peopleSlotOk`. - Pins openvibe-contracts v0.296.0 (was v0.292.0): `share.update-request@1.envelope`, `share.transfer-request@1.key_slot` and `share.member@1.key_fingerprint` are the shapes the locked sharing uses. ## 0.87.0 (2026-11-06) - **The 1.0 core surface is frozen now.** `core`, `auth`, `events`, `db` and `media` are stable before 1.0: removing or renaming one of their exports needs a major release. `docs/one-point-oh.json` keeps each name's tier, `docs/api-surface.json` records every export, and `test/surface.test.js` now pins the exact export list of the five entries, so a break there is a deliberate line in a diff rather than a surprise on a pin round. - **Removed the two `auth` leaving names**, planned for 1.0 in `docs/one-point-oh.md`: `createCodeVerifier` and `pkceChallenge`. They were the two halves of `createPkcePair`, which stays frozen; no OpenVibers repository's `main` imported them (checked 2026-11-06). Use `createPkcePair()` for both halves. - **`signDelivery` and `verifyDelivery` (the v1 signature) stay for now.** They are still the last leaving names, but `signDelivery` is imported by the test helpers of OpenVibe.Deals, Examples, Reviews, Tips, Trade and VIP on `main`, so they go once a pin round moves those onto `signDeliveryHeaders` / `signDeliveryV2`. `parseDelivery` still requires v2 by default. ## 0.86.0 (2026-11-06) - Add `openvibe-sdk/locker`: the Locker envelope (`locker.envelope@1`), end-to-end encryption for anything on OpenVibe, in Node and in the browser. WebCrypto only (`globalThis.crypto.subtle`), no other crypto library and nothing hand-rolled. - `seal` and `open` for text and bytes, and `sealStream` and `openStream` for files: a stream holds one 1 MiB chunk at a time, so a 64 MiB file costs a few MiB of memory. A stream that ends early throws at the end: discard what it yielded. - The size is padded with Padmé (PoPETs 2019): at most about twelve per cent over the real size, never below 256 bytes, and O(log log n) bits of leakage. Every chunk but the last is a whole chunk; the last holds the rest. - A people slot names the recipient's key by its RFC 7638 thumbprint, and the reader checks it before it derives anything. - The PBKDF2 iteration count is bounded on both sides: 600000 to 10000000, so a hostile header cannot make a reader work for hours. - The three locks: `people` (the content key is wrapped for each recipient with ECDH P-256 and HKDF-SHA-256), `password` (PBKDF2-SHA-256, 600000 iterations, the parameters read from the header) and `link` (the key travels in the part of the link after the `#`). - Person key pairs (ECDH P-256 for content keys, ECDSA P-256 for the fingerprint), the eight-word fingerprint (sixty-four bits, in two groups of four) and the signature that proves a person owns their signing key. - `wrapFor` and `rewrap`: adding a member to a people-locked item re-wraps the content key and leaves the content alone. - Add `openvibe-sdk/locker/server`: what a service needs to know about an envelope without opening it, the header's shape and the envelope's size, with no key and no decryption. - Add `openvibe-sdk/locker/vectors`: the shared test vectors, so the browser module (`openvibe-shared/ov-locker.js`) runs the same cases and gets the same bytes. - Add `recoveryWords(n)`: a phrase of words from the same 256-word list, one byte per word from the platform's CSPRNG, so 24 words carry 192 bits. `isRecoveryPhrase` says whether every word of a typed phrase is one of the 256, and `WORDS` is the list itself. - A people lock can now be opened by key: `open(bytes, { person })` finds the slot that names the reader's key, without a subject. An envelope with no slot for that key fails with the new code `locker.no_slot`, which says the key is not on the list (`locker.wrong_key` is for a key that is on it). - Add `inspect(bytes)`: what an envelope says about itself — the lock, the version, the padded size, the recipients' key thumbprints and the password's iteration count — without a key and without decrypting anything. - The fingerprint's two groups of four are joined by a middle dot, not a hyphen: a hyphen reads as one compound word. - Pins openvibe-contracts v0.292.0 (locker.envelope.v1 and the key directory API). ## 0.85.0 (2026-11-05) - Add server-only `openvibe-sdk/share`: PostgreSQL/PGlite store and expand-only migration, audience/role checks, exact name resolution with persistent per-caller limits, expiring membership, access requests, and recipient-bound seven-day ownership transfers. Each mutation and its append-only audit and delivery records commit atomically. Transactional owner/audience hooks update local resources. - Add uniform HTTP router, OpenAPI 3.1 fragment, CJS/ESM/TypeScript exports and `sdk.share({ base, token }).for(kind, id)` agent client. Conditional member updates detect concurrent ACL changes. Failures return generic 404 without resource content; staff overrides are audited. - Pins openvibe-contracts v0.291.0 (share.v1). No service integrations or media migration are included in this core package. ## 0.84.0 (2026-11-04) - **`openvibe-sdk/vibes`: Vibes Pay for developer apps** (MONEY.md, D12). A server creates a charge with its app token (`billing.vibes.charge`), the person pays on OpenVibe's pay sheet in whole Vibes, and Billing moves them once to the project's Vibes wallet. - `createVibesClient({appToken | tokenProvider, billingUrl})` with `charges.create({amount, description, reference, return_url})`, `charges.get(id)` and `charges.refund(id, {reason?})`. `amount` is a positive whole number of Vibes; a float, zero or negative amount is a `TypeError` before any request. The app's `reference` is the `Idempotency-Key`, so a retried create is the same charge. A token provider may be a function, an object with `getToken()`/`invalidate()`, or one with `authHeaders()`; a 401 invalidates it and retries once with the same key. problem+json becomes `VibesError` with `status`, `code`, `body` and `retryable`. - `webhooks.verify(rawBody, signatureHeader, secret)` checks the raw body in constant time against `X-OpenVibe-Signature-V2: t=,v2=.">`, and rejects a timestamp more than 300 s from the server's clock either way. The legacy `sha256=` (or bare hex) body-only form is accepted. Bad input is `false`, never an exception. The same function is exported as `verifyWebhook`. ## 0.83.0 (2026-11-03) - **`openvibe-sdk/placement`: `planAi()`, the AI Exchange face of the ONE planner** (plan-state 2026-10-22 §E, G-P1/P7; decisions D1 and D5). Not a second router: `plan()` still answers `platform.placement-result@1` and is untouched. **Off by default** — without `enabled: true` the planner answers nothing and no behaviour changes. - **The hard gates, before any scoring.** Each one is a reason, never a weight: the exact artifact (an exact id is never substituted), the commercial right (a paid managed route needs `OWN_DEPLOYMENT` or `AUTHORIZED_WHOLESALE`), the privacy class and the trust tier, the capabilities (`tools`, `json`, `vision`, `stream`), the context bound, the health, and the cost ceiling. A candidate with no rate is never assumed free: it is rejected with `no rate: the cost is unknown`. - **The objective is the cost of one success.** `cost_per_success = expected_cost / max(p_success, floor)`, so a candidate that is cheaper per token but fails more often loses to one that is dearer per token and succeeds more. `p_success` comes from optional quality evidence with a confidence: **sparse evidence is marked, not trusted** (no confidence, or fewer than 20 samples → `sparse_evidence: true`, no confidence, and the probability is pulled back to the prior), so a thin measurement can never dominate the ranking. - **Money is integer nano-USD** (decision D1), never a float of dollars: `ceil(tokens * rate_nano_per_m / 1e6)` charged once per token class (input, cached read, cache write, output) plus the per-request fee, and the cost per success rounded up to a whole nano-USD. `openvibe-sdk/cost` gains `nanoUsd`, `chargeTokens`, `expectedCostNano`, `costPerSuccessNano` and `formatNanoUsd`; the rates travel in the flat `ai.model@1` `retail_rate` shape or the nested settlement shape. - **The answer explains itself.** `explain.rejected` is `[{ id, reason }]` (as `ai.route-explain@1` names it) and `explain.objective` says what was optimised for. Every reason is stripped of keys, subject ids, hosts, IPs, URLs and paths: no secret and no internal host ever reaches the answer. - Hysteresis and affinity stay the ones `plan()` has: `current` is kept unless another candidate is clearly better (`minGain`), and `affinity` keeps a workload on the deployment it is warm on. ## 0.82.0 (2026-11-02) - **`openvibe-sdk/events` outbox: `pending()` and `rejected()` keep their count** (`countsCacheMs`, 30 s when `NODE_ENV` is production, off elsewhere), and callers at the same moment share one count. The metric gauges and readiness ask on every scrape and probe, and `rejected()` has no index, so a large outbox was read whole each time: 115 ms on Deals, 56 ms on Trade, 49 ms on Sources, 45 ms on Search and 24 ms on Reviews (pg_stat_statements, 2026-11-02). Pass `countsCacheMs: 0` to count on every call. ## 0.81.0 (2026-11-02) - **`openvibe-sdk/vision`: familiar faces (`face.familiar`), F1 of the L24 plan.** The camera owner's own Vision library, reached with the owner's token: - `list`, `create({ camera_ids, region, threshold })`, `get`, `update` and `remove` the library. The region picks strict mode (Illinois, Texas, Washington, the EU, the UK; an unknown region too) or open mode. - `enroll(libraryId, image, { name, subject })` names the crop the camera saw; in strict mode it answers a `pending_consent` person with no template and a private `consent_request.url` until the person accepts. - `match(libraryId, image)` matches only that library: a top match over its threshold, or `unknown person`. - `people`, `patchPerson` (rename or mute), `removePerson`, `requestConsent`, `checkConsent`, `acceptConsent` and `denyConsent`. - The types in `types/vision.d.ts` say what the JSON says; no template, crop or other library is ever in an answer. - **`openvibe-sdk/meme`:** the client for OpenVibe.Meme's encyclopedia, trends, analyser, deep dives and generator. - `createMemeClient(client, { baseUrl? })` (both browser and server; audience `openvibe.meme`, service id `meme`). - `search({ scope, q, status, label, kind, sort, window, channel, limit, cursor })`, `entry(slug)`, `revisions(slug)`, `trends({ window: 'day'|'week', limit })`, `analyze({ url | media_id })`, `research(topic)` (202, poll `researchGet(id)`), `saveResearch(id)` and `generate({ template | template_slug | topic, lines? })`. - `analyze` refuses locally unless exactly one of `url`/`media_id` is given, and `generate` refuses with neither a template nor a topic; no request is sent for either. Types are in `types/meme.d.ts`; the browser bundle includes the client. The `meme` namespace is on `openvibe-sdk` and in the browser bundle, and `openvibe-sdk/meme` (`createMemeClient`) is in docs/api-surface.json. - **`openvibe-sdk/app-signin`: "Sign in with OpenVibe" for developer apps, in one call.** - `createAppSignIn({ clientId, clientSecret, redirectUri, networkUrl, audience, scope, session })` gives a router with `/login`, `/callback`, `/logout`, `/me` and `/health`, built on the SDK's own auth helpers (authorization code + PKCE, the state check): nothing here re-implements OAuth. - The same recovery as `openvibe-sdk/sso` 0.73.0: a lost or expired flow and a code the Network no longer accepts start the sign-in once more by themselves; the second failure shows the same plain page (`failurePage`, now exported by `openvibe-sdk/sso`), with "Try again" and the way back. Neither ever sets a session. - The same client check: `GET /health` and `checkClient()` answer whether the Network accepts the app's credentials (a code that is never issued; cached 10 minutes; 503 only when they are refused). - The app token is verified offline against the Network's JWKS and only trusted when it was minted for THIS app (`sub = app:`), before `on_behalf_of` names the person. - The session: your own store (`session: { get, set, clear }`) or, by default, an in-memory store with an opaque httpOnly cookie. The secret, the PKCE verifier and the token never reach the browser and never appear in a log. - **`openvibe-sdk/live`: the rest of the Go live screen.** `live.slots.history(id)` (the slot's past streams), `live.slots.quality(id, { resolution, fps, bitrate })` and `live.slots.titleIdeas(id, { weekday, hour, title })` (the card's ✨ Suggest: three titles for the next stream). With these, an app does everything Live's Go live page does (https://openvibe.live/docs/streams-api). ## 0.80.0 (2026-11-01) - **`openvibe-sdk/explicit`: nudity is art.** The owner, 2026-10-31: "naked and nude is fine… stop being against art on my platform." `isExplicit()` now gates what OpenVibe.Live's own rule gates (Live#469), word for word. - **Still gated:** sex acts and pornography (sexual acts or activity, having sex, porn, masturbation, intercourse, oral sex, penetration, pornographic or sexually explicit content), and any text that puts a child and nudity together. - **No longer gated:** nudity and body words on their own. A nude figure-drawing class publishes; "a child is shown naked" never does. ## 0.79.0 (2026-10-31) - **`openvibe-sdk/machines`: the network's machines hub** (plan L34, step M2; Contracts 0.258.0). A person's machines are the network's: Bot stays the registry underneath, and OpenVibe.Network fronts it on openvibe.link/machines for the person and for the apps the person consented to. - `createMachines({ networkUrl, token, fetch, timeoutMs })`: `token` returns the caller's own bearer token — the person's session token, or an app's token holding `network.machines.read` or `network.machines.manage` with the person's consent. - `list()` → `{ machines }`, `get(id)` → `{ machine }`, `catalogue()` → `{ roles }` (what a machine may do for the network), `pair()` → `{ pairing }` (a one-time code and the command to run), `rename(id, name)` → `{ machine }`, `roles(id, roles)` → `{ machine }` (the whole set, not a change), `revoke(id)` → `{ machine }`. - A refusal throws a `MachinesError` with the hub's own `status` and `code` (`capability.denied`, `capability.owner_denied`, `registry.unknown_role`, a 404 for a machine that is not the caller's). A hub that does not answer is a `machines.timeout`, never a hang. - The machines' own capabilities stay where they are: `openvibe-sdk/bot`'s `machines.desk()` and `machines.chat()` talk to Bot itself and are unchanged. ## 0.78.0 (2026-10-31) - **`openvibe-sdk/consent`:** the client for the L24 consent registry. Nothing a person makes is off-limits to AI or data use by design; each use is off until that person turns it on in OpenVibe.Network. A service asks this check before it uses a person's content or likeness. - `createConsent({ networkUrl, token, ttlMs = 60_000, fetch, log })`: `token` returns a service token holding `network.consent.read`. `allowed(subject, purpose, { kind, client, feature })` → a boolean; `filter(subjects, purpose, opts)` → the allowed subset, in input order, in batches of 500; `onChange(event)` drops a subject's answers for a purpose on `network.consent.changed` (wire the payload or the envelope); `mock(map)` answers from `{ subject: ['purpose'] }` without the network. - **Fails closed:** a network error, the 2 s timeout, a non-200, a malformed answer or a token that cannot be minted is false for everyone asked. A failure is never cached as a yes; it logs at most once a minute. - An answer is cached for `ttlMs` (60 s), per subject, purpose, kind, client and feature; an answer in flight when a change arrives is not stored, so a revocation cannot be overwritten. - Server-side (`browser: null`): the token is a service token. Types are in `types/consent.d.ts`, the dataset-build example in `docs/consent.md`. ## 0.77.0 (2026-10-31) - **AI text may describe people** (the owner, 2026-10-31: "there should be no reason it can't describe people … remove all that crap"). `openvibe-sdk/no-looks` is deprecated. `noLooks`, `noLooksTags` and `hasLooks` return what they are given, and `RULES_VERSION` is `'off'`, so a service that still calls them keeps working until its next release removes the calls. - **`openvibe-sdk/explicit`:** `isExplicit(...texts)`, the rule that stays. AI text that says a scene shows sexual content is not published. `no-looks` re-exports it. ## 0.76.0 (2026-10-30) - **`openvibe-sdk/placement`: placement v1 — quality, the person's rules, learning and a reason (agent plan A1).** - `place(requirements, offers, ctx)` is `plan()` with the plan's road: the person's rules and ownership, a quality floor (default 0.5) with a fallback, the success estimate, cost per success, escalation and one plain sentence saying why. It returns `{ result, line, ranked, rejected, fallback }`; `result` is exactly what `plan()` returns for the offers left in, validated as today. - `learn(outcomes, {person, key, kind, ok, at})` and `successOf(outcomes, {person, key, kind, prior, now})` keep and read plain counts with a 14-day half-life, a Beta mean, Bench priors, the global weight `min(1, 20/nG)` over **everyone else's** counts (the person's rows are not counted twice) and the dotted-kind fallback. `escalate(previous, failure, requirements, offers, ctx)` handles quality, busy, offline, refused, budget and timeout, and asks the person after two escalations. It learns the failed try exactly once and returns the new counts as `outcomes`; the caller persists that set and must not record the same failure again. `optionKey(offer)` and `lineFor(facts)` are exported too. - On the person's own free machine the line claims only what the step asked for: `harness:tools` (the harness's own tools, including the signed-in CLI) says "your sign-in", and `harness:host-access` (the person's machine and its files) says "your files". With neither it says "your own machine". - `plan()` takes an optional `success: { [offer_id]: p }`: when present the cost part of the score is `estimated_cost_usd / p`, while `estimated_cost_usd` in the result stays the raw cost. When absent, scoring is unchanged. - **Behaviour fix: `user-owned` is never a default.** When `requirements.trust` is absent, `plan()` now uses `DEFAULT_TRUST` (`first-party`, `partner`, `community`, `external`). The contract (`platform.workload-requirements@1`) says exactly that: "when absent, first-party, partner, community and external. user-owned is never a default: a user-owned offer is eligible only when this list names it". A stranger's machine must never be a default. `TRUST_ORDER` keeps all five classes as the preference order; `DEFAULT_TRUST` is the four-class default. `place()` also requires a user-owned offer's `owner` to be the person of the step. - **Streaming machines.** A step that asks for a GPU or 2 or more cores excludes an offer whose `health.streaming` is `true`, with the reason "it is streaming". The field is not in `platform.resource-offer@1` yet (Link reports `screen.state` in its own status, Bot's offer does not carry it), so this release fixes the name Link can start sending; a top-level `offer.streaming` is tolerated too. - README and [docs/placement.md](docs/placement.md): the API, the rules, the estimate, escalation and the line templates. 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. ## 0.75.0 (2026-10-30) - **`openvibe-sdk/redact`: one secret scrubber for Link and AI (Memory M2).** - `redact(text)` → `{ text, count }` ports OpenVibe Link's `internal/memory/redact.go` (Node #87): the same shapes, the whole-text pass with a sentinel before the per-item pass, the unterminated private key block, Luhn card numbers, and the deliberately wide environment-assignment rule. `onlySecret(text)` says an item was nothing but a secret, so AI refuses it. - `test/redact-corpus.json` holds every case of Node's `TestRedactEveryShape`, the split and unterminated key blocks, the `.env` block and the harmless texts that must not change. Node copies the file and runs the same cases against its Go scrubber, so the two ends share one list and one corpus. - Pure functions, no dependencies: exported for Node and the browser, with ESM and TypeScript entries. 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. ## 0.74.0 (2026-10-30) - **`openvibe-sdk/account-data`: telemetry leaves with the person, and a guard finds a table nobody covers (ADR-033).** - `createAccountData` handles `telemetry_samples` by itself when the table exists: the person's samples are exported as `telemetry.json` (`at`, `service`, `operation`, `resource`, `status`, `latency_ms`), and a deletion anonymizes them (`subject` set to NULL, the sample kept) instead of removing them. A service that lists the table itself keeps its own entry. - `uncovered(db, {tables, taskKeyed, exempt})` lists every table in the current schema with a person-shaped column (`subject`, `owner`, `created_by`, `user_id`, `person`) that the table map, the telemetry handling, a task-keyed list or a reasoned exemption does not cover. A service's test asserts the list is empty, so a new table fails CI until it is covered or exempted with a reason. ## 0.73.0 (2026-10-30) - **`openvibe-sdk/sso`: a sign-in that recovers, explains itself and can be checked.** - A lost or expired flow (no or another state cookie) and a code the Network no longer accepts (`invalid_grant`) start the sign-in once more by themselves. A two-minute `ov_oauth_retry` cookie keeps it from looping, and neither ever sets a session. - A second failure, a client the Network refuses (`invalid_client`) and a Network that does not answer show a small, self-contained page in plain words, with "Try again" and "Back to ", in place of a bare line of text. - A refused client is logged as a configuration error that names `OV_OAUTH_CLIENT_ID` / `OV_OAUTH_CLIENT_SECRET`. - `GET /auth/health` and `checkClient()`: a token request with a code that is never issued. `invalid_grant` means the client is fine; `invalid_client` that its id or secret is wrong (503). The answer is cached 10 minutes. - Why: OpenVibe.Cam ran for a day on the placeholder secret from its `.env.example`. Every sign-in ended on "The Network refused the sign-in code", and nothing checked. - `siteName` option for the pages. - `legacyCookies: { access, refresh }` moves a site off its own sign-in code without signing anyone out. Its old cookies are read as the session until the next sign-in or refresh writes `ov_token`/`ov_refresh` and clears them; logout clears both. `httpOnlyAccess: true` keeps the access token httpOnly, as those sites did (their navbar asks `/auth/me`). ## 0.72.0 (2026-10-29) - **`openvibe-sdk/vision`** (VIS-5, plan §5.8b): typed browser and server calls for Vision's image tools, private search, face privacy and consent APIs, and MCP discovery. The service enforces token grants and consent; the client preserves problem errors and binary blur results. ## 0.71.0 (2026-10-28) - **`openvibe-sdk/bench`** (BENCH-4, plan §4.12): browser-safe Bench client for kinds, tasks, pinned rounds, outside entries, judgements, boards, model summaries, streamed open JSONL datasets and private evaluations. `mcpTools()` lists the live Bench MCP tools through `POST /mcp`. Calls use the `bench` service origin and `openvibe.bench` audience; optional `actingSubject` sends `X-OV-Subject`. HTTP errors remain `OpenVibeError`. ## 0.70.0 (2026-10-28) - **`openvibe-sdk/search`: the Search Fabric** (Contracts 0.225.0 `search.fabric.query`; plan 2026-10-28 §4.5). One query over several scopes, routed the cheapest way that answers. - **`search.fabric(text, { scopes, freshness, depth, answer, limit, cursor, owner, type, lang, site })`** returns `search.fabric-result@1`: - `results`: each has a scope and a kind (a document in the index, or a web page with its provider, URL and dates); - `groups`, `citations`, and `route.explain` (what each step did and what it cost). - **Scopes:** - `network` and `mine` (the person's own; a service passes `actingSubject`); - `web`: OpenVibe's open index first, then DeepSeek for a signed-in caller or a service, then Wikipedia, cached; - `news`, `images`, `video`, `products`, `people`, `places` and `code`. - `search.query(text, { scopes })` is the same call; without `scopes`, `query()` is the index search as before. - `search.providers()` lists the web ladder's `search.provider-offer@1` items. - Reads use GET, so the client may retry them. - Types: `FabricOptions`, `FabricAnswer`, `FabricResult`, `FabricExplainStep`, `ProviderOffer`; `fabricOf()` builds the query string. ## 0.69.0 (2026-10-28) - **`openvibe-sdk/tools`: domain history** (OpenVibe.Tools' `/api/v1/domains`, 2026-10-28). What the WHOIS and RDAP tools saw of a domain over time, public registration facts only (registrar, dates, status, name servers, DNSSEC; never contact details). - `tools.domains.history('example.com')` gives its snapshots and what changed when; it is null when nothing was kept. - `tools.domains.lists({ limit })` gives the most looked up domains and those of them expiring soon (each looked up five times or more). - Both read without a token; a malformed name is refused in the client. Types: `DomainHistory`, `DomainLists`, `DomainFacts`. ## 0.68.0 (2026-10-28) - **`openvibe-sdk/food`: food boxes** (OpenVibe.Food's `/boxes`, 2026-10-28). Every meal kit, ready meal and food box side by side: `food.boxes.list({ kind, diet, q })`, `.get(slug)` (with the box's codes on OpenVibe.Coupons and its score on OpenVibe.Reviews, null when there are none) and `.cost(slug, { people, meals, weeks })` (what its stated price comes to). - Each box's facts were read on the service's own pages, with the page and the day (`sources`, `checked`). - Types: `FoodBox`, `FoodBoxCost`, `BoxKind`. ## 0.67.0 (2026-10-27) - **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe Link v0.27.0's ai plugin). - `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1 kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError` with code `bot.ai_nack`. - `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models". - Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and `computer.ai_speed` (how fast the last model answered there, Link v0.29.0); `MachineChatInput` and `MachineChatResult`; `MachineCommandInput.kind` takes `ai`. ## 0.66.0 (2026-10-27) - **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`, `.computer(id)`, `.command(id, { kind, value })` (bot.command@1's computer kinds) and `.deskTurn(id, desk)`. - **`bot.machines.desk(id, { name?, kind?, profile? })`** is a desk of your own on a machine: a browser alone or a whole screen, with its own files and sign-ins (OpenVibe Link v0.19.0 to v0.26.0). - `start`, `stop` and `status`. - `browser`, `screen`, `input` and `exec` on that desk only, never on the person's own screen. `exec` takes `detach` for an app that keeps running until the desk stops. - `turn()` says whether a person is using the desk now. - Each method answers the machine's `data`. A command the machine does not do throws an `OpenVibeError` with code `bot.desk_nack`. ## 0.65.0 (2026-10-26) - **`openvibe-sdk/sso`: switch account and use another account** (the owner, 2026-10-26: sign-in "like the Google sign-in does … open to SDK and API"). `GET /auth/login?prompt=select_account` shows OpenVibe.Network's account chooser, `?prompt=login` its sign-in form, and `?login_hint=` names the account the chooser continues as when the browser has it (OpenID Connect; Network's docs/sign-in.md). `?silent=1` stays `prompt=none`; anything else is dropped. - **`openvibe-sdk/chat`: Vibe, support inboxes and feeds** (#78; the owner, 2026-10-24: OpenVibe Chat "fully modular and SDK and API"). `createChatClient` gains `vibe.*` (a person's chats with Vibe), `inboxes.*` (the team's side of a support inbox: conversations, replies, members, verified visitors, saved replies, actions, assign, translate) and `feeds.*` (the owner's side of a feed; `view` and `onLive` send no token). `createInboxVisitor(client, { inbox, token, user })` is a visitor's side with no account and no cookies (visit, send, messages, handoff, rate, sendPicture), and `createFeedPublisher(client, { feed, key })` publishes vibe coding events with a feed's key (`claudeCode(hookJson)`). - **`openvibe-sdk/themes`** (#80, #81; the owner, 2026-10-25: the theme system in the SDK and API). `createThemesClient` reads and sets the account's theme on Network (catalog with seasons, active, set, setDisplay, seasons, submissions, import), and `followTheme(fn)` keeps a page, canvas or game in step with the theme someone picked on any OpenVibe site (`readTheme()`: id, mode, tokens, display, season). `DISPLAY_OPTIONS.scheme` is `manual | auto` (Shared 4.12's Light / Dark / Auto), and each season says its light half. - **`openvibe-sdk/play`: `titles.remove`** (#82) deletes a sandbox title you manage (OpenVibe.Play#42; a developer's sandbox app registers up to 3 `sandbox-` titles since Play#41). A production title is kept (409 `play.title_kept`). - `openvibe-contracts` v0.213.0 (#79). All additions: nothing exported before changes. ## 0.64.0 (2026-10-25) - **`parseDelivery` requires a v2 signature by default.** A delivery without `X-OpenVibe-Signature-V2` is refused unless the caller passes `{ requireV2: false }`, which is deprecated. A v1 signature (`X-OpenVibe-Signature`) signs no time, so a captured delivery verified forever. OpenVibe.Events sends only v2, and every production receiver already passed `requireV2: true`, so nothing that runs changes. A test that signs with `signDelivery` and parses without options now gets `null`: sign with `signDeliveryHeaders` instead. - **`signDelivery` and `verifyDelivery` are deprecated** and leave in 1.0, with `createCodeVerifier` and `pkceChallenge` (the halves of `createPkcePair`, unused anywhere). - **What 1.0 freezes, name by name.** [docs/one-point-oh.md](docs/one-point-oh.md) puts each name of the five core entries in a tier (frozen, platform or leaving), with the reasons and how many repositories use it. `docs/one-point-oh.json` holds the lists. `test/surface.test.js` fails when a name there has no tier, and `scripts/surface.js --check` marks a breaking change with its tier. - `types/events.d.ts`: `signDeliveryHeaders` returns the timestamp and the v2 header. The type had also listed a v1 header that the function never set. - **no-looks knows nudity words** (`naked`, `half-naked`, `nude`, `undressed` and their variants), and `hasLooks` skips character names (Iron Man, Pac-Man, Spider-Man), as `noLooks` already did. Live's AI moments job titled a clip "A Naked Man Lying On Their Back With Legs" on Live's home (2026-10-24), and a Community paste read "Noteworthy Half-Naked Moment". `hasLooks` now rejects both, and `noLooks` repairs sentences as it does for "shirtless". `RULES_VERSION` changes, so services that repair stored rows run their repair again. - **no-looks knows clothes below the waist after "in"** (`shorts`, `jeans`, `pants`, `trousers`, `leggings`, `skirts`, `sweatpants`, `swimwear` and the like): "The streamer in shorts kneels on grass…" had survived every one of Community's repairs since April 2026. A shop word after it ("in the pants department") or a pocket keeps the phrase. Team kits and uniforms are not touched. - **`openvibe-sdk/front-page`** (new): `frontPageOk(text)` says whether a title or line may lead a front page (no built-in slur category, no strong profanity in five languages, no hate topic), and `containsCoreSlur(text)` is the slur check alone. The categories are OpenVibe.Chat's built-in ones, ported unchanged. An anonymous screenshot titled with a slur sat in OpenVibe.Community's "Most viewed" (2026-10-24), and Live had its own copy of the rule for its quote strip; front pages now share one. - **`no-looks` gains `isExplicit(...texts)`**: true when AI text says a scene shows sexual content (body parts, sex acts, pornography, nudity). The looks rules only remove an adjective ("a man … reaching toward their genitals"), so a service asks before it publishes AI text at all. Live's AI auto-clip 483 went public on openvibe.media with such a description on 2026-10-23. Live#344 keeps its own copy of the pattern until it pins this release. `RULES_VERSION` includes it. - **A JWKS client with no key retries by itself** on its backoff (1 s up to 5 min) until a fetch succeeds, instead of only when a token next needs checking. On 2026-10-23, Trade and the Tools gateway started before Network after the reboot and reported "no key" on `/status` for six hours, until a restart; their first sign-in would have waited for the fetch. ## 0.63.0 (2026-10-24) - **The public surface is pinned.** `docs/api-surface.json` lists every name each of the 66 entries exports and what it is; `test/surface.test.js` fails when they disagree, and `node scripts/surface.js --check` names each change as breaking (a name gone, a kind changed, a constant's key gone) or added. The first step toward 1.0's freeze of `core`, `auth`, `events`, `db` and `media`. Nothing in the package's behaviour changes. - **Outbox relays pause on a 429.** `createPgOutbox` (and so `createServiceOutbox` and the usage relay) used to hit Events' per-actor limit again on every tick after a 429, because the rows it claims next are under the same limit. Search's re-index bursts were refused 935 times on 2026-10-23, and every event was delivered only on a retry. - A 429 now pauses the whole relay until the limit's window reopens: its `retry_after_seconds`, else a minute, at most an hour. - Passes before then send nothing. The refused rows back off as before, and a 429 never rejects a row. - Nothing changes for a service that stays under its limit. - **`createServiceOutbox` prunes sent rows.** Rows sent more than `pruneAfterMs` ago (default 7 days; `false` keeps them) are deleted every 6 hours while the relay runs, and once a minute after `start()`; `out.prune()` runs it now. - Services that never called `outbox.prune()` kept every event they ever sent: on 2026-10-23, Work had 52,130 rows (119 MB) since 10-10, and Trade, Deals and Help since September or early October. - A service that already prunes itself is unaffected (a second prune finds nothing). ## 0.62.0 (2026-10-23) - **`openvibe-sdk/no-looks`: pronouns.** A pronoun is a guessed gender too, so the 27 Community summaries repaired on 10-22 that still said "he looks at the camera" or "on his device" are covered. Outside quotes: - he and she become "the person" (the verb stays as written: "he looks" → "the person looks"; he'll → the person will, he'd → the person would); - his and her before a word become "their"; him, and her after a verb, a preposition or at the end, become "them"; - himself and herself become "themself", hers and a final his "theirs". Quoted speech stays word for word. `RULES_VERSION` changes, so services that repair stored rows run their repair once more. - **`openvibe-sdk/live`: where a slot publishes.** `live.slots.profile(slotId)` reads a slot's settings and ingest (`GET /api/streams/managed/:id/profile`, owner): its stream key, Live's WHIP origin and RTMP URL, or, for a slot OpenRestream ingests, OpenRestream's own WHIP and RTMP URLs and no key. `live.whip.forSlot(slotId)` turns that into `{ url, key, authority }` to pass to `live.whip.publish()`, so going live from code no longer builds the WHIP URL by hand (OpenVibe.Examples' go-live example, plan G2). ## 0.61.0 (2026-10-22) - **`openvibe-sdk/no-looks`**: the owner's rule that public AI text never describes a person, as one shared filter. `noLooks(text)`, `noLooksTags(tags)`, `hasLooks(title)` and `RULES_VERSION`, moved from OpenVibe.Live (Live#298–#302) with its test corpus, so Community, Search, MediaHub and every service that stores public AI text run the same rules. Pure and dependency-free (Node and browsers). ## 0.60.0 (2026-10-20) - **`openvibe-sdk/host`** and **`openvibe-sdk/news`**: the public reads of the last two products without a module. Host: the hosting catalog (providers, plans, promos, community offers, domain endings with registrar prices, a name's availability, YABS benchmarks), with Host's own filters (any other key is refused). News: topics and the published and retracted stories with their sources, timeline and perspectives. Both are sent without a token; editing stays with each site's editors. ## 0.59.0 (2026-10-20) - **`openvibe-sdk/space`** (`createSpaceClient`): OpenVibe.Space for apps and CI, public in openvibe-contracts 0.209.0. Deploy a static site as a tar.gz archive or files with their paths (activate at once, or keep a preview), activate, roll back, read a deploy's log; manage projects (members as deployer, maintainer or owner), sites, their serving settings and Git source, and custom domains. An app is a principal of its own in Space and reaches only the projects a person added it to. Uploads and creations are never retried on their own. ## 0.58.0 (2026-10-20) - **`openvibe-sdk/coupons`**, **`openvibe-sdk/deals`** and **`openvibe-sdk/trade`**: what a person does for themselves on those sites, public in openvibe-contracts 0.208.0. Coupons: which store a site is, its codes, submitting a code and reporting whether one worked (what a browser extension needs). Deals: hot and new offers, posting, price observations, votes, flags and watches. Trade: instruments with their sourced observations and filings, and the person's watchlists and alert rules. Moderation stays with each site. Writes that make a new row each time are never retried on their own. ## 0.57.0 (2026-10-20) - **`openvibe-sdk/reviews`** (`createReviewsClient`): OpenVibe.Reviews for apps, whose reads became public in openvibe-contracts 0.207.0: resolve a business, product or place from its identifiers (a match or candidates), list and read entities with their aggregate, signals, summary and history, and send a correction to the editors for the person (never retried on its own). - **`openvibe-sdk/wiki`** (`createWikiClient`): OpenVibe.Wiki's reads: search, spaces, and pages with their revisions, diffs and citations, as the person may read them (without one, public pages only). ## 0.56.0 (2026-10-20) - **`openvibe-sdk/blog`** (`createBlogClient`): OpenVibe.Blog, the first product whose API only its own site could use (its capabilities became public in openvibe-contracts 0.206.0). Read a blog's published posts and feed; for the person who authorized the app, create drafts, edit (a new revision, `expected_revision` guards a race), publish, unpublish, schedule, revert and delete posts. Blog acts for an app only as its `on_behalf_of` person. Creating a post or reverting makes a new one each time and is never retried on its own. ## 0.55.0 (2026-10-20) - Six more products through the SDK, each a server module over the routes its capabilities name in Contracts: - **`openvibe-sdk/food`** (`createFoodClient`): places to get food, the food list with prices, meal plans, a person's pantry with its "what can I make" matches, and AI recipe ideas. - **`openvibe-sdk/actor`** (`createActorClient`): tasks for AI agent systems (create, list, get, cancel, and `tasks.events`, the SSE stream until the task ends), the public agent list and dry-run routing, and a person's own agents with their runs and memory. - **`openvibe-sdk/run`** (`createRunClient`): jobs on worker nodes (submit with an Idempotency-Key, list, get, cancel, `jobs.events` until `end`, a browser stream ticket), `forProject` for X-OV-Project. - **`openvibe-sdk/video`** (`createVideoClient`): OpenVibe.Video's playlists of recordings and clips, and a person's public playlists. - **`openvibe-sdk/work`** (`createWorkClient`): the job board's search and sources, saved searches, tracked applications and the agent's help with one. - **`openvibe-sdk/zone`** (`createZoneClient`): a project's object zones and their usage. - A write that makes something new each time (a plan, recipe ideas, an agent, a run, a memory note, a playlist or an item in it, a saved search, an application without a listing) is never retried on its own. Keyed writes (Run's submit, Zone's create, an Actor task with an idempotency_key, an application for a listing) and changes are. ## 0.54.0 (2026-10-20) - **`openvibe-sdk/tips`**: OpenVibe.Tips through the SDK (it had 19 capabilities and no module). `createTipsClient(client)` covers its `/api/v1`: a creator's tip page (`profiles`), tipping (`tip` → `{ interaction, checkout_url, checkout_ref }`) and paid messages, TTS, media and sound requests (`send.*`), receipts and external tips (`interactions`), `goals`, `moderation` (queue, log, moderators), `overlays` (tokens, configs, a test alert) and the supporter's own `me.export` / `erase`. Tips replays an Idempotency-Key once, so every write carries one and a retried tip is safe. ## 0.53.0 (2026-10-20) - **`openvibe-sdk/inventory`**: OpenVibe.Inventory through the SDK, as the owner asked of it ("cross site network wide shared multi purpose modular system … through sdk / api"). `createInventoryClient(client)` covers its `/api/v1`: the public reads (`kinds`, `definitions`, a person's `items` and `equipped`, `equipped([subjects])` for up to 100 at once), the person's own (`me.items`, `me.equip`, `instance_id: null` clears a slot), and an issuer's (`definitions.create` / `update`, `grants.create` → `{ instance, created }`, `instances.consume` / `revoke`). A grant is retried safely: its body's `idempotency_key` is what Inventory dedupes on. `forPerson(usr_…)` is the same client sending `X-OV-Subject` for a service acting for a person. Ids are checked against Inventory's own rules before any call. ## 0.52.0 (2026-10-20) - **`openvibe-sdk/live`**: OpenVibe.Live in the SDK, its flagship product had no module. `createLiveClient(client)` covers the routes documented at openvibe.live/documentation: who is live (`streams.list`, `recentlyOnline`), a channel (`channels.get`, `url`, `toggleFollow`), slots (`slots.create` … `regenerateKey`), going live (`goLive(slotId, opts)` → `{ stream, endpoint }`), the session (`update`, `heartbeat`, `endpoint`, `end`), the content feeds (`content.feed`, `content.moments`), and WHIP publishing (`whip.url`, `whip.publish` → `{ answer, location }`, `whip.end`). Signed calls carry the person's Network token; `/whip` gets only the stream key, never the client's token. Writes carry no idempotency key and are never retried (a second go-live is a second session). ## 0.51.1 (2026-10-17) - `openvibe-sdk/sso`: `GET /auth/me` for a signed-out visitor (or an expired, foreign or non-session token) answers `200 { user: null }` instead of 401, as Blog, Community and Wiki do. The shared navbar treats both the same; the 401 showed as a failed request in every signed-out visitor's console. ## 0.51.0 (2026-10-17) - `openvibe-sdk/commerce`: subscription tiers priced by OpenVibe.Billing (openvibe-contracts 0.201.0). `createIntent` (kind subscription) and `subscribeWithCredit` take an optional `tier`; `listTiers(creator)` reads a creator's tier prices and `setTier({ creator, tier, priceCents, status?, key })` sets one (`caps.tier` = `billing.tier.manage`; `listTiers` reads with the intent capability when `caps.tier` is not given). Without a tier nothing changes. ## 0.50.1 (2026-10-17) - `openvibe-sdk/db`: a nested `db.tx` (a savepoint) marks its scope done when it ends, like the outermost one. Async work started inside a savepoint and still running afterwards (a timer, a fire-and-forget call) no longer keeps using the transaction's connection after it goes back to the pool. - Outbox: the relay's timers run in the async context the outbox was created in. A `kick()` from inside a transaction that stays open (an inbox handler that goes on fanning out) used to make the relay's ticks run inside that transaction, on its connection. OpenVibe.Network logged "Client was closed and is not queryable" for about a minute after each go-live notice. ## 0.50.0 (2026-10-16) - `openvibe-sdk/play`: **cloud saves** — `play.saves.put(title, player, slot, { data, version })`, `get` (null when the slot is empty), `list` and `remove`, for OpenVibe.Play's new save slots (8 per player per title, 64 KB of JSON each, versioned: a stale `version` is `409 play.save_conflict`). Same token and capabilities as scores and achievements. ## 0.49.1 (2026-10-16) - Outbox: Events' `409 events.loop_detected` is a permanent refusal: the row is marked rejected instead of being retried with backoff forever (Search retried 18 such events up to 35 times each after a Reviews loop was fixed). Every other 409 is still retried. ## 0.49.0 (2026-10-15) - `openvibe-sdk/download`: `createDownloadClient({ key })` for OpenVibe.Download (the owner's "file storage and backup"): `files.list/all/get/remove`, `folders.list/create/ensurePath`, `upload({ path | data, name, folderId })` in the drive's parts, `usage()`, and `sync(dir, { to, delete, dryRun, exclude, onEvent })`, which keeps a local folder backed up (only what is new or changed: size, and sha256 when the drive has one; folders mirrored; `delete` removes drive copies whose local file is gone). Authenticates with a sync key from openvibe.download/sync, or a person's token; waits out 429s; stops on the drive's daily cap or quota with the reason. - The `openvibe-download` command (`bin`): `sync --to [--delete] [--dry-run] [--exclude NAME]`, `ls [path]`, `usage`, with `OPENVIBE_DOWNLOAD_KEY`. ## 0.48.0 (2026-10-15) - `openvibe-sdk/play`: `createPlayClient(client)` for OpenVibe.Play's game services (the owner's "game services in the SDK"): `titles.define/list/get`, `scores.post`, `achievements.unlock`, `players.adopt/achievements`, `boards.get`. Writes use the game's own principal (audience `openvibe.play`, `play.title.manage` / `play.result.write` / `play.player.read`); titles and leaderboards are public reads without a token; a player is checked (usr_… or guest:) before the request leaves. ## 0.47.0 (2026-10-15) - `openvibe-sdk/quest`: `createQuestClient(client)` → `appQuests.define(slug, quest)`, `appQuests.list()`, `appQuests.retire(slug)`, and `eventType(projectId, name)` / `projectKey(projectId)` for the `app..` events an app publishes. App quests (contracts 0.193.0, `quest.app_quest.manage`): a developer app defines quests on its own events on OpenVibe.Quest; the reward is a badge named for the app, never OpenCoins. ## 0.46.0 (2026-10-13) - `startTelemetryRetention({ db, table?, olderThanDays = 30, everyMs = 24 h, delayMs = 60 s, log? })` → `{ run(), stop() }`: the scheduled form of `pruneTelemetry()`. It first prunes `delayMs` after the call, then every `everyMs`, on unref'd timers. A service that waited a full day for the first prune never pruned when it was redeployed more often than that (found in review of OpenVibe.Blog#42). A failing prune is logged and retried at the next tick; runs never overlap; `stop()` cancels the timers and waits for a run in flight. ## 0.45.0 (2026-10-13) The telemetry sink every service stores into (plan T1, step 10). Step 5 left `createTelemetry({ sink })` and `createHttpTelemetry({ sink })` without a sink for any service but Network; `openvibe-sdk/telemetry` now ships one, a per-service PostgreSQL table (the decision and its reason are in `docs/service.md`, "Where the samples go"): - `telemetrySchema(table = 'telemetry_samples')`: the DDL for a service migration, `platform.telemetry-sample@1`'s 21 columns in schema order (no `instance`), `extra` jsonb, indexes on `(at)` and `(service, operation, at)`. - `createPgTelemetrySink({ db, table?, log? })`: the sink. One multi-row parameterized INSERT per flush; a malformed sample (no service, operation or parseable `at`, or a field of the wrong type) is dropped with one warning and the rest stored; a rejected `db.query` propagates, so the collector keeps the batch for its next flush. - `pruneTelemetry({ db, table?, olderThanDays = 30 })`: the retention delete. - A table name that does not match `/^[a-z_][a-z0-9_]{0,62}$/` throws, so no SQL goes in through it. - **Tests:** `test/telemetry-sink.test.js` on PGlite. ## 0.44.0 (2026-10-11) The SQLite kits are gone (plan T1, the deletion ledger): every OpenVibe service has run on PostgreSQL since 2026-10-08, and nothing in the estate used them. **Breaking** for a caller of what was removed: - `createOutbox` / `createInbox` (`openvibe-sdk/events`, better-sqlite3): use `createPgOutbox` / `createPgInbox` on an `openvibe-sdk/db` handle (`createDb({ pglite: true })` for a small embedded app), with `outboxSchema()` / `inboxSchema()` in its migration. - `createRevocationStore` (`openvibe-sdk/auth`, better-sqlite3 or memory): use `createPgRevocationStore`. - The stale `importSqlite` / `runSqliteMigration` type declarations (their code was already gone). - `better-sqlite3` is no longer a devDependency or an optional peer dependency, so it leaves every service's lockfile with this pin. - **Tests:** the SQLite outbox suite's two cases the PostgreSQL suite lacked move there (a token-endpoint refusal is retried, never rejected; a flush during a pass also publishes what was committed after it claimed). The delivery worker test's inbox runs on PGlite. ## 0.43.0 (2026-10-10) No sequence number from Events (Contracts 0.136.x, ADR-042 decision 7, its last step). **Breaking** only for code that read `seq`; nothing in the estate does. - **`publish()`** answers `{ event_id, cursor, duplicate }` (a repeat whose stored copy retention removed: `{ event_id, duplicate: true, pruned: true }`). - **Reads:** `pull()`/`iterate()` items and `get()` are `{ cursor, event }`. - **Realtime:** callbacks get `{ cursor }`. - **`parseDelivery()`:** `{ event, subscriptionId, attempt }`. Dedupe on `event.event_id`. - **Outbox kits:** no longer record a sequence number (a table's old `seq` column stays empty). - **Mock Events:** publish answers carry the cursor; pages, single reads, SSE data and deliveries carry no `seq` (no `X-OpenVibe-Seq`). The mock's own `platform.publishEvent()` still returns the stored `seq`, for tests that build a cursor. ## 0.42.0 (2026-10-10) An Events position is an opaque cursor and nothing else (ADR-042 decision 7, plan T7). **Breaking** for a caller that still passes a number: - **`pull()` / `iterate()`** take `after` (a page's `next_cursor`, or `latest_cursor` for the head) and nothing numeric. `afterSeq` throws a `TypeError` instead of silently reading from the start. Without `after`, a page starts at the oldest retained event, and no `after_seq` is ever sent. `iterate()` ends on the page whose `next_cursor` is `latest_cursor`, or on one that does not move the position. Pages carry `next_cursor` and `latest_cursor` only (types: `next_after_seq` and `latest_seq` are gone). - **Checkpoints** store the opaque cursor. `setCheckpoint(topic, cursor, { carrier })` rejects anything but a cursor string, and `getCheckpoint()` answers `{ consumer, topic, cursor, carrier, updated_at }` with `cursor` as stored, or null when none is. - **Realtime** resumes from the cursor exactly as Events sent it (`lastEventId` is a string; a bare number is no longer read as a position), and it dedupes by `event_id` (the last 512), never by position. The callback's `{ cursor }` is what to save; `seq` is informational and null when absent. - **`parseDelivery()`** answers `seq: null` when a delivery carries none (it was `NaN`). Dedupe deliveries by `event.event_id`. - **Mock Events** (`openvibe-sdk/testing`) behaves like Events after the change: `after_seq` is a 400, pages have no numeric fields, checkpoints hold cursors, the realtime `id:` is the cursor and a bare-number `Last-Event-ID` is ignored. ## 0.41.0 (2026-10-10) - **`createResourceIndex().list({ owner })`.** The index passes `owner` (a `usr_…`/`agt_…` subject id) to every authority on every page, beside `project` and `kind`. An authority answers only what that subject owns. A person's own resources (robots, rooms, streams, watches, Actor tasks: none of them in a project) can then be listed without reading every authority's whole index. A console keeps its own filter on the merged page, so an authority that has not added `owner` yet costs a longer walk but never shows another person's row. `docs/service.md` says what an authority answers. ## 0.40.1 (2026-10-10) - **`verifyUserToken` refuses node principals.** A node token (`actor_type: link`, `sub link:lnk_…`: a machine paired for a service, such as a robot's computer for OpenVibe.Bot) is signed with Network's key like every principal token. It was refused for service, app and module principals only, so a service that checks user tokens without an audience would have taken a node token for a session. It is `token.not_user` now, by its `actor_type` or its `sub` alone, unless the caller passes `allowServiceTokens`. ## 0.40.0 (2026-10-10) - **New: `createNetworkKeys()`** (`openvibe-sdk/auth`, server). The keys a service verifies Network tokens with, as one object: a pinned key (`publicKey`, e.g. `OV_NETWORK_PUBLIC_KEY`) used as is, or Network's JWKS (`network` or `jwksUrl`) through the process-wide JWKS client, so a token naming an unknown kid is verified after one refetch (a rotation) and the last good keys are served through a Network outage. `start()` fetches at once and retries every 30 s until the first load (Network may still be booting), then refreshes every 15 minutes; `stop()` clears both. `keys.verifyOptions` spreads into `verifyUserToken`, `verifyAppToken` and `verifyServiceToken`; `keys.loaded()` is the readiness check; `keys.keysFor(kid)` answers the keys for a token type a service checks itself. Services each had their own key store (one PEM, no kid, a 6 h refresh); this replaces them. - `verifyUserToken` and `verifyAppToken` accept a JWKS client as `jwks` (a URL, a document or a PEM as before), as `verifyServiceToken` already did. ## 0.39.0 (2026-10-10) - **New: `openvibe-sdk/notifications`** (server). `createNotificationsClient(client).push({ subjectId | userId, type, title, message, url, category, priority, icon, rich_content })` sends a person one notification through OpenVibe.Network (`POST /internal/notifications/push`, network.notification-push-request@1; capabilities `network.notifications.push` and, for a subject, `identity.subject.resolve`). It answers `{ sent: true, skipped }` (skipped: the person switched the category off) or `{ sent: false, reason: 'unknown_subject' }` for a subject Network does not know or a deleted account. The push is attempted once, because Network keeps no idempotency record for it: callers retry from their own queue. Search and Watch each had (or were about to write) their own copy. ## 0.38.1 (2026-10-10) - **`migrate`: a fresh database applies its contract migrations at once.** When a run starts with no row in `ov_migrations` (a new install, a test database, a restore drill into an empty schema), no previous release can be running against it, so the N-1 window has nothing to protect. Before, a fresh database stopped at its first contract for `windowDays`, and because a held contract holds everything after it, every migration past that point went untested: OpenVibe.Chat's test databases had stopped at 0004 since its 0005 contract. An existing database is unchanged: a contract waits for its expand's window, also when both arrive in the same deploy, and holds what follows. - `createTestDb` snapshots are built on a fresh database, so they carry the contracts too. ## 0.38.0 (2026-10-10) - **Removed: the one-time SQLite → PostgreSQL migration tooling** (plan T1, "Delete the migration era"). Every OpenVibe service runs on PostgreSQL and none calls them: `importSqlite` and `runSqliteMigration` (`openvibe-sdk/db`), `src/db/sqlite-import.js`, `src/db/sqlite-cli.js`, the `tools/asyncify/` conversion scripts and `docs/migrating-to-postgresql.md`. A consumer that still imported either function must drop it; nothing else changes. - Kept: the better-sqlite3-shaped statements on PostgreSQL (`db.prepare`), and the SQLite outbox and inbox (`createOutbox`/`createInbox`) for a small app that keeps them in SQLite (OpenVibe.Examples' webhook consumer does). ## 0.37.2 (2026-10-10) - `openvibe-sdk/realtime`: each event's callback gets its opaque `cursor` (the SSE id Events sent) beside `seq`: `subscribe(topics, (event, { seq, cursor }) => …)`. Save the cursor after handling the event and pass it back as `lastEventId` to resume, so a subscriber never needs the numeric seq (plan T7). `cursor` is null when Events sent no id. Additive. ## 0.37.1 (2026-10-10) - `openvibe-sdk/testing`'s mock Events answers a pull with `latest_cursor` (the head as an opaque cursor), as OpenVibe.Events does since Events#45. A consumer that starts at "now" stores it and reads on with `after=`, never needing the numeric `latest_seq`. `events.pull()` documents it. Additive. ## 0.37.0 (2026-10-08) - **`openvibe-sdk/account-data`: `startSubscriptions()`**, the boot-time half every adopter had copied. It creates any missing OpenVibe.Events subscription for the two account topics (or `topics`) to the service's loopback endpoint with its own token (audience openvibe.events, scope events.subscription.manage), retried with backoff in the background, and leaves an existing one alone, so a restart creates nothing. It returns null when the Events URL, the delivery secret or the client secret is unset; `stop()` ends the retries and `done` says whether every topic was subscribed. ## 0.36.0 (2026-10-08) - **`openvibe-sdk/account-data` (new, node): a service's share of account export and deletion (ADR-033).** Six services implemented it by hand; every newer one (Actor, Food, Help, Work, Quest, Rent, Watch…) had none. A service now declares the tables that hold a person's rows (`{ table, subject, value?, file?, columns?, erase? }`, with `erase` `'delete'`, `{ anonymize: {…} }` or `{ keep: '' }`) and gets `apply(event, { send })` for network.account.export_requested / network.account.deleted, a signed `consumer()` handler for POST /internal/events, `exportPart()`, `erase()` (one transaction, merged-account aliases included), `ensureSchema()` / `ACCOUNT_DATA_SCHEMA` (account_data_events: idempotent per export/deletion id, so a redelivery never erases twice and a failed confirmation is sent again) and `createNetworkSender()` (the service's own client-credentials token for Network's internal routes). Identifiers in the table map are checked when it is made; values are always parameters. ## 0.35.2 (2026-10-08) - **The mock developer platform grants `services.release.manage|read` (`openvibe-sdk/testing`, patch):** openvibe-contracts 0.113.0 moved the developer console from OpenVibe.Codes to OpenVibe.Services and retired `codes.release.*`; the mock's app catalog follows, so a developer-path run against it requests the names Network grants. Pins openvibe-contracts v0.113.0. ## 0.35.1 (2026-10-08) - **A PGlite test database no longer keeps its process alive (`openvibe-sdk/testing`, patch):** PGlite emulates PostgreSQL's SIGALRM timers with `setTimeout` (its wasm runtime's `__setitimer_js`), and one loaded from a snapshot (0.29.0) keeps rescheduling a 10 s startup-progress alarm, so a test file that ended without closing its database never exited and a per-file runner timed it out (OpenVibe.Network's whole suite, OpenVibe.Community's unclosed tests). `createTestDb` on PGlite now unrefs the timers `__setitimer_js` sets (told apart by the caller's frame, once per process); queries run synchronously in the wasm and never wait on one. Every other timer, a test's own included, stays ref'd. A service-side workaround that unrefs every PGlite timer (OpenVibe.Media's preload) can go. - **`createTestDb` on the containers unrefs its lease connection only once the lease is held (patch):** 0.31.1 unref'd the socket before `pg_advisory_lock` answered, so a process with nothing else open (no PGlite alarm since the fix above) exited mid-setup with code 0, its remaining tests never run. ## 0.35.0 (2026-10-07) - **Events public origin moves to `https://openvibe.events` (plan T7):** realtime clients and the mock platform now use the new origin by default; SDK examples and types name it as well. Browser pages using the SDK's realtime client must allow `connect-src https://openvibe.events` in their Content Security Policy. Events serves both origins during migration; the old origin will answer 308 after clients have moved. ## 0.34.0 (2026-10-05) - **`openvibe-sdk/resources` (new): the resource index and the control plane (ADR-048, plan T13 step 2).** `parseResourceName`/`resourceName`/`resourceNameOf` reuse the pinned openvibe-contracts helpers (`contracts.resources`, required lazily) for the one resource name, OVRN `ovrn:::/`. `createResourceIndex({ authorities, token, fetch, pageLimit, concurrency, timeoutMs, maxPages, maxMs })` fans out `GET {authority}/api/v1/resources?project=&kind=&cursor=&limit=`, follows each authority opaque `next_cursor` and merges the `common.resource-list-result@1` pages in authority order; an authority that fails (network, timeout, non-2xx, malformed page) never fails the call — its pages read so far are kept and it is reported in `stale`, so a read model can be rebuilt. A cursor chain that would not end is bounded — a repeated cursor (a cycle or an echoed cursor) or an empty `next_cursor` ends the walk, `maxPages` caps the pages read and an optional `maxMs` deadline caps the time — and the authority is reported `stale` (`sdk.bad_response`/`sdk.timeout`) rather than hanging `list()`. `createResourceClient({ origin, token })` POSTs a `common.resource-control-request@1` to `/api/v1/resources/control` and returns the authority `common.resource-control-result@1` (`done`/`pending`/`refused`/`failed`, a refused one carrying its problem or `confirmation_required`); a refused/failed result is returned even when it arrives with a non-retryable non-2xx (403/409/422), so a confirmation gate is not thrown away. An idempotency key is generated when the caller omits it and a transient failure (network, timeout, 408/425/429/5xx) is retried with the same key and body, so an action is never applied twice; an error thrown after the retries carries that `idempotencyKey`, so the caller's own retry reuses it instead of duplicating a sensitive action. `RESOURCE_KINDS` lists the kinds whose three-letter id prefix ADR-048 has chosen (`media.object`/`med`, `watch.watch`/`wch`); the proposed (`act`, `run`, `zon`) and unchosen (`codes.repo`, `events.queue`, `events.subscription`) kinds are deliberately absent until step 8. ## 0.33.0 (2026-10-05) - **`openvibe-sdk/events` surfaces the opaque `after`/`next_cursor` pull cursors (ADR-042 decision 7):** `pull()` now takes `after` — a page's opaque `next_cursor` — and sends it as the `after` query parameter; `afterSeq` (the numeric `after_seq`) stays for one release and is sent when no cursor is given. `StoredEvent` carries `cursor` and `EventsPage` carries `next_cursor`. `iterate()` starts from `after` or `afterSeq` and advances on `page.next_cursor` (falling back to `next_after_seq` while the compatibility field remains) after `onPage`, so a page that matched no events still advances the saved cursor; resuming from a saved `next_cursor` replays nothing. The mock platform (`openvibe-sdk/testing`) models the same page shape and accepts `after`, and `README.md`'s pagination note names the cursor. New assertions in `test/events.test.js` and `test/app-events.test.js` cover the round-trip, the empty page, and the `after_seq` fallback. - `iterate()` keeps the mode the caller asked for (a string `afterSeq` such as `'5'` is still the numeric position) and finds the head without the numeric compatibility fields: once a server sends only cursors, the page whose `next_cursor` does not move ends the walk. ## 0.32.0 (2026-10-05) - **`openvibe-sdk/usage` gains `createUsageReporter` (plan T1 step 7):** the shared reporter Tools, AI, Events and Bot each rewrote. `createUsageReporter({ db, service, source, table = 'usage_outbox', billingUrl, tokenClient, audience, fetchImpl, timeoutMs, intervalMs, batchSize, now, log })` owns one service's readings: `key(...parts)` is `usageKey(service, …)`, `sample(fields)` fills in the reporter's `service` and `source` and defaults `id` to the idempotency key, and `record(t, reading)` validates the reading against `platform.usage-sample@1` (via `validateUsageSample`, so a missing openvibe-contracts never blocks the queue) and inserts it idempotency-keyed in the outbox table inside the caller's transaction (`db.tx`'s handle; a replayed key changes nothing). The relay is `createPgOutbox` pointed at Billing's `billing.usage.record` (`POST /api/v1/usage`, one reading per request): `start`/`stop`/`kick`/`flush`/`prune` plus `pending`/`rejected`, and it uses the given token client (`createServiceTokenClient`, audience `openvibe.billing`). A reading is never dropped — a relay that cannot reach Billing (down, no grant yet, a missing route, a timeout or a 401/403/404/408/425/429/5xx) retries with backoff across restarts — and only Billing refusing the reading itself (any other 4xx: 400, 402, 409, 410, 413, 415, 422) marks a row rejected: kept with its error and never sent again, so nothing is billed twice; a 401 drops the cached token first. The reporter bills whatever reading it is given, so callers must not record first-party or sandbox traffic: `requireProject: true` makes `record()` refuse a reading without a `prj_…` project. Without `billingUrl` or `tokenClient` readings still queue and only the relay is off. Re-exported from the top-level entry and declared in `types/usage.d.ts` (the `./usage` subpath already existed); `outboxSchema(table)` stays the migration DDL. New unit test for key derivation, replay refusal and the retry/reject relay (the full status matrix, 401 invalidation and the timer stop). ## 0.31.1 (2026-10-05) - **`createTestDb` in pg mode no longer keeps a test process alive (`openvibe-sdk/testing`, patch):** the lease `pg.Client` it opens to hold the run's advisory lock (since 0.26) kept a ref'd socket, so a test file that finished without `process.exit()` never exited and a per-file runner timed it out (Media's test:pg hung at 300 s per file). The lease socket is now unref'd right after it connects; the lock still holds for as long as the process lives, and `close()` still releases it. ## 0.31.0 (2026-10-05) - **`openvibe-sdk/service` carries the HTTP telemetry middleware and autoscaling signals (plan T1):** lifted from OpenVibe.Network's `server/telemetry.js` and `server/observability.js`, which were the only implementation. `createHttpTelemetry({ service, sink, intervalMs = 15000, routeLabel?, skipExact?, skipPrefixes?, skip?, signals? })` returns a collector with `middleware()`, `record`/`gauge`/`count`, `requestStarted`, `requestFinished`, `observeRequest`, `flush` and `stop`. A request is not a row: requests aggregate per `route|method|status_class` per flush into `count`/`sum`/`max`/`p95`, and one `http.request` `platform.telemetry-sample@1` sample per key is emitted per flush (its mean in `latency_ms`, the counts in `extra`), plus the active-requests, rolling-p95 and event-loop-lag gauges once per flush — never per request, so the SDK buffer cannot overflow. A sample carries the route template, method and status class, never a raw URL or token; health, readiness, metrics and static assets are skipped (`telemetrySkipped`). The singleton `telemetry.init(...)` / `telemetryMiddleware` / `telemetry.stop()` wires it process-wide: `init` starts the event-loop monitor and the unref'd flush timer, `stop` stops both and flushes once (a `gracefulStop` stop step), and requiring the kit starts nothing. `registerSignals({start, stop, lag})` injects a service's own monitor. No change to the other modules. A request that matched no route is labelled `'unmatched'` (never its raw path), `skipPrefixes` defaults to `['/shared']` (Network passes `['/shared', '/api/chrome']`), the per-flush aggregation keys and latency samples are capped (overflow folds into the `'other'` route label; p95 uses reservoir sampling while count/sum/max stay exact), and a second `init` stops (flushes) the collector it replaces. ## 0.30.0 (2026-10-04) - **`openvibe-sdk/bot` (new, both; plan B8):** the OpenVibe.Bot client. `createBotClient(client, {actingSubject?})` wraps `robots` — `list`/`get`/`create`/`update`/`delete`/`pairingCode`, `operators.list`/`add`/`remove`, `devices` (listing), `streaming.get`/`set`, `audit`/`iterateAudit`, `estop`/`clearEstop` and `command` (`bot.command@1`, answered by Bot's own idempotency cache) — plus `devices.rotate`/`revoke`, the public `profiles`/`kits` reads, and `pair` (the agent redeems a one-time code; never retried). A service acts for a person with `X-OV-Subject` (`bot.robot.read`/`manage`/`control`, `bot.device.connect`); a browser calls as the signed-in person. Browser-safe, with its ESM entry, types and the browser bundle, and a test over the Contracts bot fixtures. The operator WebSocket and the internal Run→Bot job dispatch are not wrapped. ## 0.29.0 (2026-10-04) - **`createTestDb` reuses a migrated snapshot (`openvibe-sdk/testing`, minor):** on PGlite, the first test process boots, migrates, runs the new `seed: async (db) => {}` hook and saves the data directory (`dumpDataDir`) under `os.tmpdir()/openvibe-test-snapshots` (`OV_TEST_SNAPSHOT_DIR`); later processes load it (`loadDataDir`) instead of migrating, refresh `ov_migrations.applied_at` (so an ADR-028 contract migration stays held as on a fresh migrate) and run `migrate` only if the snapshot is behind. The key is the migrations' file names and contents, the SDK and `@electric-sql/pglite` versions and `seedKey` (default: the seed function's source; a `seedKey` without a `seed` is not the same snapshot as one with both). Parallel processes build it once under a lock file; the others wait up to 60 s, then migrate on their own. A file that does not load is deleted and rebuilt. The result adds `snapshot` (`'hit'`, `'built'` or `'off'`) and `setupMs`. Each database is still the process's own: rows a test writes are never in the snapshot. **Opt out** with `OV_TEST_SNAPSHOT=0`. On `store: 'pg'`, `seed` runs on the run's schema after the migrations with the schema owner's rights (as on PGlite) and `setupMs` is reported; roles, privileges and the lease are unchanged. ## 0.28.0 (2026-10-04) - **`openvibe-sdk/commerce` (new, server; plan T5 step 13):** the Billing client Tips and VIP each carried a copy of. `createCommerceClient(config, { caps, fetchImpl, tokenClients, timeoutMs })` with `createIntent` (purchase or subscription), `createTransfer`, `refundTransfer`, `subscribeWithCredit`, `cancelSubscription`, `getSubscription`, `listSubscriptions`, `entitlement`, `rates` and `baseUrl`. One call shape: an `Idempotency-Key` is sent verbatim, a 401 invalidates the token and retries exactly once with the same key, and a problem+json answer becomes a `CommerceError` (`status`, `code`, `body`; `retryable` for no status, 5xx, 429, 401 and `billing.frozen`). `intentKey(prefix, parts)` derives a key and `receipt(kind, fields)` builds an outbox record (never posted). `openvibe-contracts` is required lazily, only when a token client is created, so the tarball still loads with no dependencies. No change to the other modules. ## 0.27.0 (2026-10-04) - **`openvibe-sdk/cost` (new, server; plan T1):** `rateCard()` / `validateRateCard()` for `platform.rate-card@1`, `costSnapshot()` / `validateCostSnapshot()` for `platform.cost-snapshot@1`, and `estimate(card, quantity, unit)` for the charge a rate card gives a quantity. - **`openvibe-sdk/runtime` (new, server; plan T1):** `runtimeOffer()` / `validateRuntimeOffer()` for `platform.runtime-offer@1`, and `runtimeClass()` / `validateRuntimeClass()` / `RUNTIME_CLASSES` for `platform.runtime-class@1`. - **`openvibe-sdk/storage` (new, server; plan T1):** `storageOffer()` / `validateStorageOffer()` for `platform.storage-offer@1`. - **`openvibe-sdk/delivery` (new, server; plan T1):** `deliveryOffer()` / `validateDeliveryOffer()` for `platform.delivery-offer@1`. - **`openvibe-sdk/node` (new, server; plan T1):** `nodeCapabilities()` / `validateNodeCapabilities()` for `platform.node-capabilities@1`, and `nodeOffers(descriptor)`, which derives a node's resource and runtime offers. - **`openvibe-sdk/placement` exports `TRUST_CLASSES`** (the five T1 trust classes) and `TRUST_ORDER` (the planner's default preference, unchanged), with their type declarations. - **`openvibe-sdk/usage` (new, server; plan T1):** `platform.usage-sample@1` readings in one place. `usageSample(fields)` builds the record — `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit` and every optional dimension pass through, `at` defaults to now (ISO) and `source` to `openvibe-sdk/usage`, null fields are stripped and no money field is invented (`cost_estimate`, `free_allowance_used`, `vibes_charged` are the caller's; rating is Billing's). `usageKey(service, ...parts)` joins an idempotency key with `:` (the service alone when there are no parts; a non-empty service is enforced). `validateUsageSample(record)` → `{ ok, errors }` (`openvibe-contracts` is required lazily on first use; a missing package never claims validity: `ok: false`, `errors: []`). `openvibe-sdk/govern` now builds its `onUsage` record with `usageSample()`: the record is unchanged. - **`openvibe-sdk/telemetry` (new, server; plan T1):** `platform.telemetry-sample@1`, the universal operation observation. `telemetrySample(fields)` and `validateTelemetrySample(record)` as in usage; `createTelemetry({ service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000 })` → `{ record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop() }`: a collector that buffers samples and flushes them to `await sink(samples)` every `intervalMs` (an unref'd timer) and on `stop()` (clears the timer, flushes once — a `gracefulStop` stop step). `record` builds `{ service, instance, operation, at, latency_ms, ...labels }`; a value that is not a number goes to `extra[name]` as a scalar. A sink that rejects is logged once and its samples kept for the next flush; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute. The SDK holds no network code: the sink is the service's. - **`openvibe-sdk/placement`:** `TRUST_ORDER` gains `user-owned` (between `first-party` and `partner`; the T1 trust classes): a `user-owned` offer is eligible under `trust: ['user-owned']` and for workloads that name no trust list. - The devDependency `openvibe-contracts` moves to v0.86.0 (it adds a `node` principal to `identity.service-token-claims`); the copied types and the lockfile integrity hash follow. - **`openvibe-sdk/govern` emits a full usage record** (plan T5). `createGovernor({ service, provider?, resource?, region? })`; `reserve()` takes optional `operation`, `provider`, `resource`, `region`, `trace_id` and `route_epoch`, which override the defaults. `onUsage` now gets a `platform.usage-sample@1` reading: `id` (the reservation id), `idempotency_key`, `service`, `project`, `subject`, `resource`, `provider`, `region`, `operation` (default `reserve`), `quantity`, `unit`, `at`, `route_epoch`, `trace_id`, `source` (`openvibe-sdk/govern`); absent values are left out. **Changed:** `amount` is now `quantity` and `state` is gone (the schema allows neither; only new reservations are emitted). No money fields: rating is Billing's. A governor created without `service` still works and logs one warning (`log`, default `console`). Tested against openvibe-contracts 0.86.0. ## 0.26.0 (2026-10-01) - `openvibe-sdk/service` (new, server; plan T1): the service kit. `gracefulStop({ name, server, stop, close, drainMs = 4000, deadlineMs = 5000, deadlineExitCode = 1, signals = true, exit, log, beforeDrain, handles })` → `{ stop(signal?), stopping() }` and `within(ms, promise)`: OpenVibe.Network's and OpenVibe.Community's `server/graceful.js` (stop steps, then `server.close` with `Connection: close` on requests in flight, idle connections closed every 50 ms and event streams destroyed, then close steps, exit 0; one stop per process), plus the exit code of a blown deadline (the 5 s family and Media exit 0), `beforeDrain` and `handles` (closed last; a failure exits 1). Its deadline timer is not `unref()`'d, so a step stuck on a promise ends in the deadline's exit code rather than a quiet exit 0. The error layer: `createServiceError(name)`, `ServiceError`, `asServiceError(err, { publishing, map })`, `run(fn, status)`, `wrap(fn)`, `sendError(res, req, err, log)`, `jsonBody({ limit = '512kb' })` (malformed: 400 `request.invalid_json`; too large: 413 `request.too_large`), `privateNoStore(res)` and the opt-in `jsonErrors()`, with Reviews/Wiki's defaults and `extra` spread (or `extra: 'details'` for Tips/VIP). `createReadiness`, `skip`, `safeReason`, `createRegistry`, `instrument`, `metricsHandler`, `isLoopbackDirect`, `releaseInfo`, `createRelease` (openvibe-shared) and `problem`, `sendProblem` (openvibe-contracts) are re-exported, required on first use from the service's own packages: the SDK gains no dependency. Recipe per family: `docs/service.md`. - `scripts/esm.js`: a getter on a CommonJS entry (a lazy re-export) becomes an ESM function that reads it on call, so importing `openvibe-sdk/service` loads neither package. ## 0.25.2 (2026-09-29) - `openvibe-sdk/realtime` follows Events' opaque cursors (ADR-042): it resumes from the last SSE id exactly as Events sent it (a cursor, or a seq from an older Events) and dedupes on the `seq` in the message data. With cursor ids the previous release parsed the id as a number, so its resume position and repeat filter stopped advancing. `subscription.lastEventId` is now that raw id (a string). ## 0.25.1 (2026-09-29) - `verifyServiceToken({ jwks })` also takes a JWKS client the service already holds (`jwksClient(url)`, or one a test injects), so a service that built one for `openvibe-sdk/sso` verifies service tokens with the same keys, chosen by `kid`, instead of taking the first key itself. ## 0.25.0 (2026-09-29) - `openvibe-sdk/sso` (new, server): `createSsoClient({ site, baseUrl, clientId, clientSecret, networkUrl, networkInternalUrl, secureCookies })` → `router(express)` (`/login`, `/callback`, `/fedcm`, `/logout`, `/me`, `/refresh`), `optionalAuth()`, `requireAuth()`, `verify()`, `extractToken()`. A product site's sign-in with OpenVibe.Network in one call: state and PKCE S256 on every sign-in, the code exchanged server-side, `next` limited to this site and the Network, offline verification through the shared JWKS client. Replaces the copies in Blog, Coupons, Deals, Host, News and Trade (plan T1). - `openvibe-sdk/events` `createServiceOutbox({ db, source, eventsUrl, clientId, clientSecret, eventTypes, validate })`: a service's events outbox in one call around `createPgOutbox` (`emit` joins the change's transaction, `emitIn(t, …)`, `moderationAction`, `start`/`stop`/`kick`/`status`); the relay is off, and says so, without the events URL or the client secret. Replaces the wrapper nine services copied (plan T1). - No floating promises (plan T0): the Valkey queue logs a job whose run rejects outside its handler (a malformed stream entry was an unhandled rejection), and the mock jobs service fails a job whose background run throws. ## 0.24.0 (2026-09-29) - `openvibe-sdk/auth` `verifyServiceToken(token, { jwks | publicKey, issuer, audience, contracts, acceptSandbox, log })`: a service or app token for a service that receives them. The SDK picks the key (the one the `kid` names, else each, from the shared JWKS client or a pinned PEM); every rule is the service's own pinned openvibe-contracts `serviceAuth.verifyServiceToken`, passed in as `contracts`, so the SDK carries no copy of the rules. No key is `token.unavailable` with the fixed reason 'signing key not loaded yet' (the fetch error goes to `log`). Replaces the hand-written kid-and-contracts loop in AI, Search, Sources, Games and Codes (plan T1). - `verifyUserToken` refuses a typed token (`typ` or `purpose` set: Network's realtime ticket, a FedCM assertion) as `token.not_user`: it is never a session, whatever its issuer and audience say. Network's session tokens carry neither. ## 0.23.1 (2026-09-29) - The release version is whole again: `SDK_VERSION` and the browser bundle say 0.23.1 (0.23.0 shipped them at 0.22.0, so version, bundle and pack tests failed on its tree). - JWKS: a malformed entry in `keys` (null, a non-object) is skipped instead of marking the whole key set failed; a legacy document with only `public_key` no longer refetches every 30 s for a token that names a kid. - Types: `log` is declared on the verify options (it is passed to the JWKS client). ## 0.23.0 (2026-09-29) - **JWKS client** (`openvibe-sdk/auth`: `jwksClient(url, opts)`, `createJwksClient`, `jwksStatus()`), now behind `verifyUserToken`/`verifyAppToken` for a JWKS URL (plan T0/T1: one refresher instead of hand-written copies that swallowed errors). Keys stay fresh for 6 h; after that the last good keys are served while one refresh runs in the background, and a failed fetch keeps them and backs off (1 s doubling to 5 min) instead of rejecting sign-ins or hammering Network. A token with an unknown `kid` refetches at once (a rotation is honoured immediately); unknown-kid refetches are spaced 30 s apart, so a flood of made-up kids costs one fetch per 30 s. Only state changes are logged (`log` option: the first failure, the recovery). `status()` / `jwksStatus()` report readiness, staleness, failures and the next try for `/api/ready`; `start({ intervalMs })` refreshes in the background on an unref'd timer. Before: an expired cache plus a failed fetch rejected every token with 503, and each unknown kid forced a refetch. ## 0.22.0 (2026-09-28) - **`openvibe-sdk/placement`** (roadmap WS-Z9, the adaptive fabric): `plan(requirements, offers, { rateCards, states, current })` places a workload on the best eligible node or provider and explains it (platform.placement-result@1). Hard constraints first (capabilities, trust, residency, health, latency ceiling, capacity, cost ceiling; excluded candidates carry the reason), then the objective (cheapest, lowest-latency, balanced; private and first-party-only never leave first-party capacity; critical work stays with its authority). `marginalCost` prices work in a provider's real billing unit after its free allowance, forecast to the end of the period, with a reserve kept for high-priority work; owned capacity is ~free until its binding resource is busy, then work spills over. Hysteresis keeps a placement unless an alternative is clearly better (15% by default); failover is immediate. `rendezvous` (weighted highest-random-weight hashing), `pickTwo` (power of two choices), and Ed25519-signed route plans: `signPlan`, `verifyPlan`, `createPlanHolder` (keeps the last valid plan when a new one is bad, expired or older). - **`openvibe-sdk/govern`** (roadmap WS-Z1): weighted cost units (common.resource-cost@1) with quotas per subject and tier over minute, hour, day and month windows; `reserve` → `commit(actual)` or `release`, settled by any process; idempotency keys so a retried charge counts once; concurrency leases. Valkey holds the counters (atomic Lua); without Valkey they live in the process. ## 0.21.3 (2026-09-28) - `openvibe-sdk/testing` `createTestDb`: on the containers it also returns `url` and `directUrl`, the `DATABASE_URL` and `DATABASE_DIRECT_URL` of its schema, for a test that spawns a process of the service (OpenRe's transport workers); `null` on PGlite. - `tools/asyncify/sqlfix.py`: the outbox-envelope rewrite no longer nests on a second run. ## 0.21.2 (2026-09-28) - `openvibe-sdk/events` outboxes (SQLite and PostgreSQL): a `flush()` called while a relay pass runs now gets the next pass, which starts when that one ends. It used to share the running pass, so it could return before the rows committed after that pass claimed were published (a caller's "flush, then assert both went out" saw one). Calls made during a pass share that next pass; `stop()` waits for both. - `scripts/test-services.sh`: PgBouncer closes idle server connections after 5 s and takes at most 120 per database, and PostgreSQL allows 200. Each test process has a role of its own, so the pools of a suite with many files ran PostgreSQL out of connections. ## 0.21.1 (2026-09-28) - `openvibe-sdk/events` `createPgOutbox`: the relay publishes the rows it claimed in the order they were written. `UPDATE … RETURNING` gives no order, so a batch could reach Events out of order (a `media.clip.failed` before the `media.vod.ready` written before it). Found converting OpenVibe.Media. ## 0.21.0 (2026-09-28) - `openvibe-sdk/auth`: `createPgRevocationStore(db, { table })` keeps Network's per-person token cutoffs on PostgreSQL. `await load()` reads them into memory at boot, so `isRevoked()` and `cutoffFor()` stay synchronous on the request path; `apply()` and `record()` are async and write through before memory moves, never backwards (a later cutoff stored by another process wins). `revocationSchema(table)` is its table, for the service's migration (the SQLite store created its table at first use, which a PostgreSQL runtime role cannot do). - `openvibe-sdk/valkey`: the client no longer runs iovalkey's ready check (`INFO`), which a service's ACL user may not call; every connect logged NOPERM. `ready()` still pings. ## 0.20.4 (2026-09-28) - `openvibe-sdk/db`: `bytea` comes back as a `Buffer` from both adapters. PGlite returned a `Uint8Array`, so `Buffer.isBuffer` and `.toString('hex')` differed between tests and production. Found with openvibe-shared's config store on PostgreSQL. `test/db-ambient.test.js`. ## 0.20.3 (2026-09-28) - `importSqlite` reads the target tables of the connection's current schema, not only `public`. A per-run test schema from `createTestDb({ store: 'pg' })` works now, so a rehearsal can use the containers. Production is unchanged: its search path is `public`. - Rehearsal note: an embedded PGlite (`--pglite`) can break on very large rows. OpenVibe.AI's `runs` did, while the same import verified on PostgreSQL. When `--pglite` reports every table empty, rehearse on the containers instead (`docs/migrating-to-postgresql.md` §6). ## 0.20.2 (2026-09-28) - **`importSqlite` refuses a source table that has no target table.** Before, such a table was skipped without a word. A table that a module created at runtime and the migration forgot would have been dropped by the switch (found converting OpenVibe.AI: `subject_credentials`). SQLite's own tables and FTS5 virtual tables with their shadow tables are still skipped, because an index is rebuilt, not copied. Anything else not imported on purpose goes in the new `skipSource` option, also on `runSqliteMigration`. An audit of the eleven services already switched found nothing lost. `test/db.test.js`. ## 0.20.1 (2026-09-28) - **`openvibe-sdk/db`: one query at a time per transaction connection.** `await Promise.all(xs.map((x) => db.….get(x)))` inside a transaction used to put several queries on the transaction's client at once. node-postgres queues them, but warns that pg@9 will refuse. Such calls are now chained per connection, in call order. Outside a transaction, each call still takes its own pool connection and runs concurrently. `test/db-ambient.test.js` checks this on PGlite and through PgBouncer. ## 0.20.0 (2026-09-28) - **`db.afterCommit(fn)`** (and `t.afterCommit(fn)` on a transaction handle) runs `fn` once the running transaction commits. It never runs after a rollback, and a savepoint that rolls back drops the hooks added inside it. Hooks run in order, outside the transaction, with the connection already back in the pool; one that throws is logged and the rest still run. Outside a transaction, `fn` runs on the next turn. It replaces the better-sqlite3 pattern of a `setImmediate` after a synchronous transaction, for calls to other services that must see committed state (News' comment-thread visibility was the first user). - **`db.stats().open`**: transactions in progress on this handle. - `tools/asyncify` gains `sqlite-schema-to-pg.js` (a service's SQLite DDL → `migrations/0001_initial.sql`) and `g1pass.py` (the non-codemod edits of a Blog-shaped service). `asyncify.js` now treats `{ name: someAsyncFunction }` as an async method `name`. None of these are in the published package. ## 0.19.0 (2026-09-28) - **`openvibe-sdk/db` `runSqliteMigration({ service, sqlite, directUrl, migrations, tables, argv })`** is a service's `scripts/migrate-to-postgres.js` in one call. It: - applies the migrations as the owner, or on an in-memory PGlite with `--pglite` for a rehearsal; - imports with `importSqlite` into emptied tables; - makes text PostgreSQL refuses storable (a NUL is dropped, an unpaired surrogate becomes U+FFFD, in text and inside JSON), and reports every column where that happened; - verifies counts and checksums, prints the report (`--json` for all of it), and answers 0 only when everything verified. It was generalised from Tips' script and first used by Wiki. `test/sqlite-cli.test.js`. ## 0.18.1 (2026-09-28) - **`db.prepare`:** a statement with one parameter takes a single array argument as that parameter's value (`WHERE id = ANY(?)`). Only statements with several parameters read a single array as the list of values (better-sqlite3's array form). Found converting OpenVibe.Wiki. - **`openvibe-sdk/testing`:** - `createTestDb({ migrations, store, service })` gives a migrated database for one test run. It is PGlite by default. With `store: 'pg'` (or `OV_TEST_STORE=pg`) it runs on the containers, with roles and a schema of its own shaped as the host's `add-service.sh` makes them. - Setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple concurrently updated"). - `open()` gives a second pooled handle, and `close()` ends the roles' backends and drops them. - `createTestValkey({ prefix })` gives the containers' Valkey under a prefix of its own. - Every service's copied test helper is replaced by these. ## 0.18.0 (2026-09-28) This release makes the move from better-sqlite3 mostly mechanical: call sites keep their shape and gain `await`. - **`openvibe-sdk/db`: ambient transactions** (on by default; `createDb({ ambient: false })` turns them off). - Inside `db.tx(fn)`, plain `db.*` calls join the running transaction through AsyncLocalStorage. So do `db.prepare` statements and library stores that were handed `db`. Their writes are visible inside the transaction, roll back with it, and never wait on it. - A `db.tx` inside is a savepoint. - A promise the transaction did not await runs on the pool once it has ended. - `db.detached(fn)` leaves the transaction on purpose, and `db.inTransaction()` tells whether code is inside one. - The handle `fn` receives works as before. - **`db.prepare(text)`**: async statements shaped like better-sqlite3's. - Methods: `get` (a row or `undefined`), `all`, `run` (`{ changes, rows, lastInsertRowid }`; the id needs `RETURNING`) and `pluck()`. - Parameters: `?`, or `@name`/`:name` from one object (a repeated name is one parameter). - The compile to `$n` leaves `'…'` strings, `"…"` identifiers, dollar quotes, comments and `::` casts alone. - Tests: `test/db-ambient.test.js`, on PGlite and through PgBouncer. - `importSqlite` verification treats a JSON `null` and SQL NULL as equal. JSON text `'null'` imported into `jsonb` had failed its checksum (found in the Tips migration). - The guide gains a section of lessons from the Tips migration: `SUM(bigint)` returns a numeric string, test roles through PgBouncer, byte-exact replays, idempotency claims, NUL and surrogate cleaning, and pub/sub without Valkey. ## 0.17.0 (2026-09-28) - **`openvibe-sdk/geo`** (browser and server): `createGeoClient(client, { samples?, timeoutMs? })` finds the closest OpenVibe Link (roadmap WS-X1 task 5). - `nodes({ role?, region? })` reads Network's public node registry, `GET /api/v1/nodes` (`network.node-list-result@1`), without sending a token. - `measure(nodes)` times each node's beacon several times (the best sample counts, since the first also pays for DNS and TLS) and returns the nodes fastest first. - `nearest({ role?, region?, preferRegion? })` returns the fastest measured node that is up or degraded. When nothing can be measured, it falls back to `preferRegion`, then to the first healthy node, and returns `measured: false`. - The root export and the browser bundle include `geo`. The root export also includes `openre`. ## 0.16.0 (2026-09-28) - **`openvibe-sdk/openre`** (server): `createOpenReClient(client, { baseUrl?, publicUrl?, playbackTtlMs? })`, the client for OpenRe.Stream, the platform's streaming engine. Products stop carrying their own copy, starting with OpenVibe.Live. - `streams`: `list`, `byExternalRef`, `create`, `get` (null when missing), `update`, `delete`, `keys`, `rotateKey`, `destinations` and `addDestination`. - `destinations`: `update`, `delete`, `test`, `start`, `stop` and `logs`. - `sessions`: `list`, `get` (null when missing), `playback` (cached 10 s), `end` and `outputs`. - Also `outputLogs`, `workers` and `manageUrl`. - Every call uses a service token for `openvibe.openre`. `{ subject }` names the person a call acts for (`X-OV-Subject`). ## 0.15.0 (2026-09-28) **The event outbox and inbox on PostgreSQL** (ADR-004 on ADR-035), in `openvibe-sdk/events`. The better-sqlite3 versions stay for services that have not moved yet. - **`createPgOutbox(db, { events })`:** takes an openvibe-sdk/db handle. - `enqueue(t, envelope)` takes the transaction handle, so the event exists if and only if the change commits. - The relay claims due rows with a lease (`FOR UPDATE SKIP LOCKED`), so any number of processes and hosts relay one table without sending an event twice. A relay that dies leaves its rows to the next one once the lease expires. - Otherwise it behaves as before: backoff on transient failures, permanent refusals isolated per row, the seq kept, `pending`, `rejected` and `prune`. - **`createPgInbox(db)`:** `once(consumer, eventId, async (t) => …)` runs the handler once, in one transaction with its receipt, and a failed handler leaves no receipt. - **`outboxSchema()` and `inboxSchema()`:** the DDL to put in a service migration, since the runtime role cannot create tables. - **Quieter tests:** the slow-query log is off by default on PGlite, whose first query includes its WASM start. - **Tests:** `test/pg-outbox.test.js` runs on PGlite, and through PgBouncer with three relays sharing one table (60 events, none sent twice). ## 0.14.0 (2026-09-28) **The async data layer and Valkey modules** (ADR-035 and its amendment; roadmap WS-X2): every service moves to PostgreSQL 18 and Valkey, async-first. - **`openvibe-sdk/db`:** - **Queries:** `createDb({ url | pglite, service })` has `query`, `many`, `maybe`, `one`, `value` and `exec` on a safe `sql` tagged template: every value is a bind parameter, fragments nest, and `sql.ident`, `sql.set`, `sql.insert`, `sql.join` and `sql.json` help build them. - **Transactions:** `tx(fn, { isolation, retries })` retries 40001 and 40P01 with backoff, and nested `t.tx()` calls are savepoints. - **Health:** `ready()` reports the store that actually answered, with the pool state. There are metrics (`db_query_seconds`, pool gauges) and a slow-query log that shows text only, never values. - **Adapters:** node-postgres, which production uses through PgBouncer in transaction mode, and PGlite for tests (real PostgreSQL in-process). Both return the same rows: int8 as Number (refused beyond 2^53), timestamps as ISO strings, dates as `YYYY-MM-DD`, numeric as exact text, json parsed. - **Migrations:** `db.migrate({ dir })` runs numbered SQL files labelled `expand`, `migrate` or `contract`. It records them in a ledger with checksums, refuses edited or out-of-order files, serialises runs with an advisory lock, holds a contract migration until its expand is 7 days old (the N-1 window), and supports `-- no-transaction`. - **`importSqlite()`:** the one-time move of a service's SQLite file into its PostgreSQL schema. - Tables go parents first, and values are converted by target type (0/1 to boolean, text or epoch to timestamptz, JSON text to jsonb, JSON arrays to arrays). - Identity values are kept and sequences advanced. A source column that would be lost is refused unless dropped on purpose. - Streamed verification compares counts and content checksums. - **`openvibe-sdk/valkey`:** the shared connection. It is auto-pipelined, confined to `VALKEY_PREFIX`, has a `duplicate()` for blocking consumers, and `ready()`. - **`openvibe-sdk/cache`:** `getOrSet` loads a key once across processes (single-flight plus a short lock), with tags, TTLs and an in-process fallback. A cache error never fails the caller. - **`openvibe-sdk/queue`:** at-least-once jobs on Valkey streams. It has consumer groups, retries with exponential backoff, a dead-letter stream, delayed jobs, a 24-hour dedupe by id, and reclaiming of a job a dead or hung worker holds. It also has an in-process twin. - **`openvibe-sdk/pubsub`:** fan-out across processes and hosts, inside the prefix. - **`openvibe-sdk/limits`:** `createValkeyLimitStore(valkey)` makes every process and host count an actor together, in one atomic script per request. If Valkey fails, that request is counted in-process instead. - **Dependencies:** `pg`, `@electric-sql/pglite`, `iovalkey` and `better-sqlite3` are optional peers, loaded only by the module that needs them. - **Tests:** `scripts/test-services.sh up` starts PostgreSQL 18, PgBouncer and Valkey 9 in containers, and `test/db.test.js` and `test/valkey.test.js` run against them when their URLs are set. Otherwise they run on PGlite and the in-process stores, and print skip lines. ## 0.13.0 (2026-09-28) **Deliveries are v2 only**, as OpenVibe.Events sends them since 2026-09-28 (the v1 header, a replayable HMAC of the body alone, is retired: shim C-60). `signDeliveryHeaders` returns `X-OpenVibe-Timestamp` and `X-OpenVibe-Signature-V2` only, and the mock platform's `deliverEvents` sends no `X-OpenVibe-Signature`. A consumer test that asserted the v1 header must drop that assertion. `signDelivery` and `verifyDelivery` stay for testing a legacy receiver, and `parseDelivery` still accepts a v1-only delivery unless `requireV2`, which every production consumer sets. ## 0.12.0 (2026-09-28) **`openvibe-sdk/limits`** (roadmap WS-R task 4): per-actor rate limits at a service's capability boundaries, where nginx can only limit by address. `createActorLimiter({ limits: { minute, hour, day } })` returns `limits(name, ownLimits)`, Express middleware that counts each actor in fixed windows and refuses past a limit with 429 problem+json (`rate_limited`, `Retry-After` from the tightest exceeded window) before the route runs. - The actor is the verified principal (`svc:live`, `app:…`), else the signed-in person (`user:`), else `ip:
`; `actor(req)` overrides it, and null skips counting. - A route's limits replace the defaults window by window. - Counters are per process and bounded (`maxActors`, oldest dropped first), and `onLimited` observes refusals. Node only. Additive. ## 0.11.0 (2026-09-25) Two more clients (roadmap WS-F task 4). **`openvibe-sdk/chat`**: OpenVibe.Chat over REST as the token's person: `global.send/history/search`, `rooms.list/create/get/update/messages/send/deleteMessage/join/leave/read/members/setMember`, `dms.list/create/get/messages/send/deleteMessage/read/unread/blocks/block/unblock`, `exportMine()`; writes are never retried, a room or conversation you may not read is `null`. **`openvibe-sdk/ai`**: OpenVibe.AI's run API: `runs.create(workflow, input, { wait, version, idempotencyKey, target, attribution, onBehalfOf, options })`, `runs.get/list/cancel/retry/citations/addCitations`, `runs.waitFor(id)` (polls until succeeded, failed or cancelled), and the direct operations `chat/generate/summarize/classify/extract/enrich/embed`; a create is retried only with an idempotency key. Both in the browser bundle, ESM and types. Additive. ## 0.10.0 (2026-09-25) **`openvibe-sdk/search`** (roadmap WS-F task 4): OpenVibe.Search's query API. `createSearchClient(client)` gives `query(text, { owner, type, lang, filter, facets, limit, cursor })` (one page: `results`, `next_cursor`, optional facet counts), `iterate(text, opts)` (every result across cursor pages, `max` to stop early), `suggest(text)` and `document(owner, type, id)` (null when missing or not yours to see). Anonymous callers get public documents; a signed-in person also gets restricted documents naming them; a first-party service with `search.query.delegate` passes `actingSubject`. Several values of one facet go as repeated `facet.` parameters. In the browser bundle too. Additive. ## 0.9.1 (2026-09-24) The v0.9.0 tag reports `SDK_VERSION` 0.8.0 and ships a stale browser bundle; 0.9.1 is the same API with both right. Use 0.9.1. ## 0.9.0 (2026-09-24) `openvibe-sdk/auth` **`createRevocationStore(db?)`**: token cutoffs from Network's `network.user.token_valid_after` (Contracts 0.39.0), so a service refuses a person's older tokens as soon as they sign out everywhere, change their password or are banned. `apply(event)`, `isRevoked(claims)`, `cutoffFor(subject)`, `record()`; SQLite-backed or in memory. Additive. ## 0.8.0 (2026-09-24) `openvibe-sdk/frame`: **the OpenVibe Frame**, the navbar, footer and themes every OpenVibe site sits in, for any app, first-party or not. It covers: - the universal navbar: one account menu, notifications, the site switcher, and sign in and out through your own session endpoints (`sessionUrl`, `loginUrl`, `logoutUrl` with `{path}`/`{url}`); - the shared footer, with network and legal links, the "shipped X ago" line and an Updates link; - the theme loader. These are the same files every OpenVibe site runs, served by OpenVibe.Network from the published openvibe-shared release, so nothing is bundled into your app. `mountFrame(opts)` loads and mounts them in the browser. It never throws for a network failure; a part that cannot load resolves to `null`. `frameTags(opts)` returns the same markup as strings for server-rendered pages, with the configuration escaped for its `