openvibers/openvibe.sdk CHANGELOG.md

CHANGELOG.md

main 177.3 KB · 2187 lines · markdown

1# Changelog
2
3## 0.166.0 — 2026-11-13
4
5**`scripts/test-services.sh` without Docker: the same services from local binaries** (CI checks on shared nodes). A
6check job on a shared node runs a repository's GitHub Actions job with act in host mode, with no Docker daemon and no
7root, so `eval "$(node_modules/openvibe-sdk/scripts/test-services.sh up)" && npm test && npm run test:pg`, the test line
8of nearly every OpenVibers repository, could not start its containers there.
9
10- With no working Docker (`docker info` does not answer), `up` starts PostgreSQL 18 (initdb, SCRAM, fsync off),
11 PgBouncer (transaction mode; each test role is looked up with `auth_query` and SCRAM is passed through) and Valkey from
12 local binaries, as the current user, under one throwaway directory (`OV_TEST_SERVICES_DIR`, default
13 `$TMPDIR/ovsdk-services-<uid>`), on the same ports with the same users, and prints the same three variables. A second
14 `up` reuses what runs; `down` stops them and removes the directory (and the containers, when Docker is there).
15- `OV_TEST_SERVICES=docker|local` chooses; `OV_TEST_SERVICES_PATH` limits where the binaries are looked for. Ubuntu:
16 `apt install postgresql-18 pgbouncer valkey-server` (their own services can stay off).
17- Verified on ov-node-01 as an unprivileged user with no Docker: `testing-db`, `db`, `pg-outbox`, `db-ambient`,
18 `valkey` and `govern` all ran against the local services (no case skipped) and `down` left nothing behind. Tests:
19 `test-services` (no Docker and no binaries says what is missing and starts nothing; `OV_TEST_SERVICES` overrides).
20
21## 0.165.0 — 2026-11-13
22
23**`openvibe-sdk/agents/soul`: who an agent is** (agents.soul@1, Contracts 0.424.0; Design rules 175
24`actor-is-the-work`, 176 `souls-grow-by-yes`). The owner, 11-13: "you should have different bots … they each have a
25soul … your agents are created based on your memories and personality". Pure functions:
26
27- `validate(soul)`: the shape and the rules a schema cannot say (no secret in a soul; no permission in a soul, which
28 is the blueprint's hands and rules).
29- `toPrompt(soul, { name })`: one stable block of plain words (the dials in words, habits, beliefs, pushback, never,
30 notes), placed before memory so a provider's cached prefix never changes inside a conversation; it says the soul
31 never widens what the agent may do.
32- `toSoulMd`, `toIdentityMd`, `fromSoulMd`: the SOUL.md and IDENTITY.md layout OpenClaw and Hermes read, out and back;
33 a SOUL.md from elsewhere has its sections mapped (Core Truths, Boundaries, Vibe…) and the rest kept as notes.
34- `change(soul, patch)`: an agent's suggestion for its own soul, as a proposed soul and the change in words; nothing
35 is applied (its person says yes).
36- Tests: `agents-soul`.
37
38## 0.164.0 — 2026-11-12
39
40- **New `openvibe-sdk/ai-meter`:** every product meters its AI calls the same way (ai-metrics.md; Contracts 0.421.0
41 `ai.call.recorded@1` 1.1.0 and `ai.call-report@1`).
42 - `startCall()` measures each call on its own clock: TTFT and latency less any queue, tokens per second after the
43 first token (or Ollama's own decode time), tool calls, fallbacks, the error class, and the charged price when the
44 provider gives one.
45 - `normalize()` reads the usage of OpenAI chat completions and Responses, Anthropic Messages, Gemini and Ollama.
46 The input it reports is the uncached input, with cache reads and writes and reasoning tokens apart.
47 - `createMeterClient()` batches the records to AI's `POST /api/v1/usage/calls` with the service token.
48 - It retries a network error, a 408, 425, 429 or 5xx with backoff, and asks for a new token after a 401.
49 - A batch refused for any other reason is dropped and counted, never sent again.
50 - AI prices any call that brings no price and publishes it.
51
52## 0.163.0 — 2026-11-12
53
54From reading 24 open agent projects (one-system-v2.md). Three new modules, all pure functions:
55
56- **`openvibe-sdk/agents/rules`:** permissions as data, from Gemini CLI and OpenCode (agents.permission-rule@1).
57 - `decide()` lets the last matching rule win; with no rule, the run's level decides.
58 - Unattended work never waits on an ask. It settles to allow at the levels approve-for-me and full, and to deny below
59 them.
60 - The floor always asks; with nobody there, it is denied.
61 - `fromBlueprint()` compiles a blueprint to rules: only the tools it names, never `.git/hooks` or its own config,
62 and its sandbox and network.
63- **`openvibe-sdk/agents/router`:** pays for the strong model only when it is likely to matter, in RouteLLM's shape
64 over OpenVibe Decisions.
65 - It asks for a calibrated probability that the small model is not enough, and routes against a threshold
66 calibrated to a budget (the share of steps that should go strong).
67 - It draws the quality-cost points the scoreboards show.
68 - The strategy chain is the person's pick, then the blueprint's pin, then the router. When Decisions is down, the
69 cheaper model runs.
70- **`openvibe-sdk/agents/metrics`:** what a run measured (agents.run@1 1.4.0).
71 - The charged cost comes first; an estimate counts only where nothing was charged, and unknown is null, never 0.
72 - It records tokens with cache reads and writes, steps, latency, a named exit and evidence.
73 - Also `passAtK` (unbiased), `passHatK` and `flakeRate`.
74
75## 0.162.0 — 2026-11-12
76
77- **New `openvibe-sdk/agents/blueprint`: make your own agent** (agent-blueprints.md; Contracts 0.419.0's
78 `agents.blueprint@1` and `agents.install@1`). These are pure functions, so a browser, Link, a CLI and every service
79 check and run a blueprint the same way:
80 - `validate`: the shape a person edits, plus the rules a schema cannot say. It carries no secret (anything that
81 looks like a key or a token is refused where it is). Every `{{input}}` is declared. Level `full` has a budget.
82 Machine and robot tools have somewhere to run. A shared place says what it needs, and never names a machine. A
83 tool served over MCP has its shapes. A team never contains itself.
84 - `withDefaults`: it talks only, asks first, keeps nothing, answers its owner, and spends 25¢ a run and $2 a day.
85 - `asks`: the install screen in plain words, with the keys an install records as agreed. `unagreed` lists what an
86 install has not agreed to yet.
87 - `wider`: what a new version asks for that the old one did not. That includes a tool going from ask to allow, a
88 higher level, a bigger budget, more people who may ask, more memory, a new place, trigger or team member. An
89 empty list means an install set to "same-or-less" takes the version without asking.
90 - `fill`: inputs, typed and checked.
91 - `compile`: one run. It returns `agents.run-start@1`, the loop's system prompt (with the floor said in words) and
92 tools, the Dispatcher's need, pin, rungs and price cap, the places and the install's machines, memory, and the
93 team. Try it, or a run without an install, asks before every tool and spends at most 5¢.
94 - `estimate`: the price of a run on the cheapest choice it may use, through `agents/dispatch`, and the rungs on
95 which it is free.
96
97## 0.161.0 — 2026-11-12
98
99- **`openvibe-sdk/cam`: `capture(options)`** opens a camera through the site's own shared capture core (Shared's
100 `ov-capture.js`, OVCapture).
101 - It loads the core once from the calling site's own `/shared/` (never another origin) and resolves to a
102 MediaStream. A page that already has OVCapture uses it.
103 - The options are OVCapture's own: mode, facing, lens, mic, resolution, audio and video.
104- **Cam's in-page kit** (`CAM_KIT_SRC` / `CAM_KIT_SRI`) is re-pinned to the kit Cam serves today, checked against
105 the live file at its versioned address.
106
107## 0.160.0 — 2026-11-12
108
109**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
110a run's thread. It runs chat and ask turns on a server with no workspace (Vibe answering, an agent using OpenVibe's own
111products as tools).
112
113- **`runLoop(options)`** resolves `{ messages, stop, error, usage, cost, turns, toolCalls }`. Stop is `done`,
114 `terminated`, `budget`, `turns`, `aborted` or `error`.
115 - **The model adapter:** `complete({ messages, tools, system, signal, budgetLeft, onDelta })` returns the next turn:
116 text, thinking, tool calls, why it stopped, usage and cost. `onDelta` streams the turn as it is written.
117 - **Tools** run in parallel, or in sequence when the loop or a tool asks. Parallel runs are prepared in order, run
118 together, and their results kept in the model's order.
119 - **`beforeToolCall`** is the approval gate (block, with a reason, or end the run); **`afterToolCall`** may rewrite a
120 result.
121 - **`steering()`:** words the person sent while it works, taken before the next turn. **`followUp()`:** words after
122 it would stop. **`finishTurn()`** may end or continue the loop. **`transformContext()`** compacts or filters the
123 thread before each turn, and entries marked `ui_only` never reach the model.
124 - **The budget** (usd, tokens, turns, steps) stops it between turns, never inside one; the model is told what is left.
125- **What it keeps, whatever the hooks say:**
126 - A call cut off by the output limit never runs; the model is asked to send it again.
127 - An unknown tool, input its schema refuses, a throw or a timeout comes back to the model as an error, never as a
128 crash.
129 - The same call repeated past `repeatLimit` gets a reminder instead of a run, and an idempotent tool answers the same
130 call from the run's memo.
131 - A long output is clipped before the model sees it; the whole result stays in the thread.
132- **`createStepSink({ append, update, delta, startSeq, startTurn, runId })`** turns the loop's events into
133 `agents.step@1` steps. In order: the person's words, the thinking (folded), one tool step updated when it ends, then
134 the agent's words streamed into their step. A person's message opens the next `turn` (Contracts 0.417.0).
135- **`checkInput(schema, value)`:** the JSON Schema subset tools use.
136- **New `openvibe-sdk/agents/dispatch`: the Dispatcher** (agent-engine.md §3), as pure functions over a person's
137 choices:
138 - `ladder(choices, { need, pin, avoid, budgetLeft })` orders them: their own machine, a signed-in plan, their own
139 key, the free allowance, then credits, cheapest first inside a rung, and a smaller model before a stronger one
140 unless the step needs strength. What cannot do the step (a capability, the context, the speed, the budget) is
141 listed with why. A person's pick wins when it can, and is otherwise passed over with the reason.
142 - `escalate(current, evidence, choices)` moves to a stronger model only on checkable evidence: a failed test,
143 schema, tool or grader, or the model's own "not sure". A grader's failure also changes family.
144 - `harnessFor(task, harnesses, { plans })` chooses by capability: the loop for turns with tools, the person's
145 signed-in plan, else dsh.
146 - `placeFor(run, { machines, cloud })`: the person's computer first (free), a Codes runner for a repository
147 elsewhere, an OpenVibe computer when allowed, Bot for a robot.
148 - `estimate(choice, tokens)` and `slice(parentBudget, share)`.
149- **New `openvibe-sdk/agents/api`: the Run API from a product's own store** (server).
150 - `createRunApi({ product, subject, store, prefix })` returns `handle(req, res, next)` for Express or any Node http
151 server: the list, start, get, steps, events, messages, stop and approvals routes of Contracts' `docs/agents-runs.md`.
152 - The store has `list, get, steps, start, say, stop, answer` and, optionally, `follow` to push its own events
153 (otherwise the stream reads new steps). A method a store lacks answers 409.
154 - No subject is 401, another person's run 404, and a start without words or with an unknown kind 422.
155 - Every run carries its ref. A store's own `{ status, code }` errors go out as they are; anything else is a 500 that
156 says nothing of the store. A mandate from a caller is dropped (intake's hook sets it).
157 - Read back with the SDK's own client in its test.
158- Ported from pi's agent loop (earendil-works/pi, `packages/agent/src/agent-loop.ts`, MIT, Copyright (c) 2025 Mario
159 Zechner): the inner loop over tools and steering, the outer loop over follow-ups, the turn hook, parallel and
160 sequential tools, and the two tool hooks. The model API, the message shape, the budget, the memo, the repeat guard,
161 the timeouts and the step sink are OpenVibe's.
162
163## 0.159.0 — 2026-11-12
164
165The agent engine's client (Contracts 0.414.0: everything a person asks an AI to do is a run with a thread of steps).
166
167- **`openvibe-sdk/agents` runs:** `runs.list({ kind, status, q, limit, cursor, before, agent, parentRun })`,
168 `runs.get(run)` (null on 404), `runs.steps(run, { after, limit })`, `runs.start(text | agents.run-start@1,
169 { idempotencyKey })`, `runs.say(run, text | { text, attachments })` (continue a conversation or steer work),
170 `runs.stop(run, { reason })`, `runs.events(run, { after })` and `runs.follow(run, { onRun, onStep, onDelta,
171 onApproval, onEvent })`, which resolves with the end status.
172- **`approvals.answer(run, approval, answer)`** by the approval's id, its object, or the step seq that asked.
173- **`createFrameAgentsClient(client)`:** every product's runs through the Frame (`/api/frame/runs…`), where a run is
174 named by its ref (`<product>:<id>`); `createAgentsClient(client, { prefix })` for any product's Run API.
175- **Helpers:** `refOf`, `parseRef`, `isEnd`, `needsPerson` (a run waiting for a message is a conversation between
176 turns, not something that needs its person), `KINDS`, `STATUSES`, `END_STATUSES`.
177- **The stream:** the engine's events (`run`, `step`, `delta`, `approval`, `end`) are read beside AG-UI's; only a
178 step advances the resume cursor, so a snapshot or more text that shares a step's id is never skipped.
179- **The plugin interfaces** (model adapter, tool, sandbox, memory, planner, reviewer) stay as JSDoc types; the loop
180 that calls them is the next release.
181- `run`, `post`, `events`, `desks`, `takeOver`, `handBack` and `replay` are unchanged.
182
183## 0.158.0 — 2026-11-12
184
185- **`openvibe-sdk/views`: one view API for every content kind** (Contracts `media.view-request@1`, the `media.viewed`
186 and `media.engaged` events).
187 - `createViewsClient(client).record(event | [events])` posts a preview, watch or live heartbeat for
188 `ref: { service, kind, id }` to Media's `POST /api/v1/views`, up to 20 at once. `seconds` is cumulative for the
189 session and item, so a retry does not count twice; a signed-out viewer keeps an opaque session id.
190 - `.counts(ref)` reads one item's `{ views, engaged, previews, preview_share, peak_concurrent }` from
191 `GET /api/v1/views/counts`. Media never stores an IP address or a user agent with a view, and a preview counts
192 from 1 second.
193
194
195## 0.157.0 — 2026-11-12
196
197See it work: a run's desks, control and replay
198
199- Add to `openvibe-sdk/agents`: `desks(run)` → the run's desks with their view and still, `takeOver(run, desk)`,
200 `handBack(run, desk, note)` and `replay(run)` → the steps with the still each saw. They call the same-named routes
201 under the client's service (`/api/v1/runs/:id/desks/...`), so a page and a script make the same calls. No tag: the
202 main session releases this after the sites' stage lands.
203
204## 0.156.0 — 2026-11-12
205
206- **`openvibe-sdk/exchange`: reviewed compute prices, keyless.**
207 - `createExchangeClient(…).computePrices({ kind })` and the standalone `createComputePriceClient()` read
208 `GET https://openvibe.exchange/api/compute/prices`: each GPU or computer's hourly and per-second price, the
209 provider's own price page, the date it was checked and the date it must be reviewed again, filtered to
210 `kind: 'cpu' | 'gpu'` when asked.
211 - The public endpoint needs no key, sends `Access-Control-Allow-Origin: *` and is cached for 60 seconds. The
212 keyed client still speaks the API host (unchanged); its price read goes to `siteUrl` (default
213 `https://openvibe.exchange`) without its developer key.
214
215## 0.155.0 — 2026-11-12
216
217- Add `design.icons.suggest(label, { context, limit, rank })` to `openvibe-sdk/design`: the best glyphs for a button's
218 words by meaning (the glyph's name, its META keywords and its other-set names), with `rank: true` asking a small model
219 to order the shortlist when the words are new. The answer is the same shape as `GET /api/v1/glyphs/suggest`, including
220 `new`, `ranked`, `model` and, when known, `draft_price`.
221- `design.icons.get()` and `.search()` are public now, like the Design API they call: no access token. `.draft()` still
222 needs one, and the client still sends a token when it has one.
223
224## 0.154.0 — 2026-11-12
225
226- Add the Frame's data client to `openvibe-sdk/frame` (and `sdk.frame` in the root, browser and ESM entries):
227 `createFrameClient(client)` gives `choices(task)`, `query(q, { scopes })`, `providers()`, `ask(body)` and
228 `events(run)`, the same calls every site's composer makes through OpenVibe.Network. `choices` answers the Model sheet
229 (OpenVibe's models enriched from Exchange, the person's keys, computers and plans); `ask` starts a question and
230 `events` reuses `agents.events` for its AG-UI stream, whose `ANSWER` event carries the numbered citations. Types are
231 in `types/frame.d.ts`. The shared navbar, footer and `online()` are unchanged.
232
233## 0.153.0 — 2026-11-11
234
235- Add `objects.rotate(id, { degrees })` to `openvibe-sdk/media` (the v2 object client): turn a stored JPEG, PNG or WebP
236 picture clockwise by 90, 180 or 270 degrees. Media keeps the uploader's original beside the turned copy, every
237 generated size follows, and only the picture's owner may turn it (`subject` / `actingUserId` on a service client).
238
239## 0.152.0 — 2026-11-11
240
241A train of two pull requests: `openvibe-sdk/elements` (#227) and `openvibe-sdk/ui-layout` (#226).
242
243- Reserved for `openvibe-sdk/ui-layout` in OpenVibers/OpenVibe.SDK#226. That module is not released by this entry; it
244 keeps the changelog's minor sequence while that pull request is open, and its own entry replaces this one at merge. (#227)
245- Add `openvibe-sdk/ui-layout` (also `sdk.uiLayout` in both entries): `validate(catalog, layout)` is the one
246 check the server, the editor and the AI answer use, returning `{ ok, reasons[] }` in plain words. It refuses a
247 missing, hidden or undersized required part, anything pressed under 44 px (and any part under the catalog's own
248 pixel minimum, e.g. Stop at 64), a part covering a required part or breaking a `never_covers` rule, a tint that
249 is not a catalog theme token or a style/binding the part does not allow, a part the catalog does not have, and
250 any URL, markup or code. `applyDefaults(catalog, device?)` builds the valid default arrangement (required parts
251 first, every part visible, 44 px met) and `merge(layout, patch)` applies a patch by part id without mutating
252 the layout. Browser-safe, no dependencies; exhaustive tests. (#226)
253
254## 0.151.0 — 2026-11-11
255
256- Add related-origin passkey sign-in to `openvibe-sdk/sso` (opt-in `relatedPasskeys: true`): `POST /auth/passkey/options`
257 and `POST /auth/passkey/verify` broker a WebAuthn ceremony for this site's exact origin through OpenVibe.Network as
258 its OAuth client. The random browser binding stays in the site's own `__Host-` cookie; Network verifies the exact
259 origin and RP ID and returns a single-use result that becomes the site's ordinary session. Browsers without
260 `PublicKeyCredential.getClientCapabilities().relatedOrigins` keep the existing OAuth/passkey path.
261
262## 0.150.0 — 2026-11-11
263
264- 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.
265- Type the image task's owner daily cap and guest find switch in `AiTaskDefaults`.
266
267## 0.149.0 — 2026-11-11
268
269- 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.
270
271## 0.148.0 — 2026-11-11
272
273- #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`.
274- #210: add `feed.list({ kind, cursor })` for the cross-site Community Feed, keep the existing Community reader callable, and type preview video media.
275- #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.
276- #212: add `agents` SDK methods to fetch run snapshots, follow resumable AG-UI event streams and post approvals.
277- #213: add `wallet.changes()` for the owner-scoped Vibes, OpenCoins and channel-points gain and loss window, and
278 `wallet.markRead()` for each currency's own read cursor.
279- #214: add browser-safe `player.current()`, `player.handoff(url)` and `player.on(listener)` for a live stream carried
280 between OpenVibe sites by Shared's floating player.
281- #215: add typed Chat readers for a person's compacted day, week and month memory, plus personal erase and opt-out.
282- #216: add `exchange.createCatalogSearchClient().search(q, filters)` for public model and provider offers (prices,
283 measured speed and Bench quality when available), and type the image search filters and source metadata.
284- #217: add the owner-only agent sheet reader and Actor pause, resume and memory controls through Network's relay.
285- #219: the app sign-in test checks a token's shape, not the letters "eyJ" (a macOS flake).
286- #220: `homeFeed.network.popular({ limit })` (the signed-out front door's five things people do now) and
287 `homeFeed.network.tryNext({ limit })` (the signed-in person's own suggestions).
288- #222: type the exact reply target and its display name on a Community comment; a thread's first page includes two
289 replies and the rest use the parent cursor.
290- #207: add `codes.tasks` for the Dock's Code tab: `list({ limit })`, `create({ text, mode, repo?, attachments? })` and
291 `stop(id)` (`codes.task-summary@1`).
292- openvibe-contracts pinned at v0.406.0.
293
294## 0.147.0 — 2026-11-10
295
296- #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.
297- #182: add the `system` and `can_run` machine metadata to the machine reader type.
298- #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.
299- #202: add `chat.embeds(message)` and `readEmbeds()` for the typed `chat.embed@1` address previews on a chat message.
300- #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.
301- #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.
302
303## 0.146.0 — 2026-11-10
304
305- #183: add the private history reader for cursor reads, search, summaries, agent recent reads, settings and owner-scoped deletes.
306- #184: add `frame.online()` with member presence, anonymous guest counts and live channel/site items.
307- #186: add `quest.today()` for the authenticated daily Quest response.
308- #195: add `bot.embedUrl(robotId, mode)` for panel, overlay and compact embeds.
309- #196: add merged code search, cited deep answers and cursor iteration to Search.
310- #197: add filtered, paged Inventory holdings reads and counts, plus async holding pages.
311
312## 0.145.0 — 2026-11-10
313
314- `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.
315
316## 0.144.0 — 2026-11-10
317
318- `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.
319
320## 0.143.0 (2026-11-10)
321
322- **`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`.
323
324## 0.142.0 (2026-11-10)
325
326- **`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`.
327
328## 0.141.0 — 2026-11-10
329
330- `community.comments.upload(ref, file)` uploads a Community paste comment attachment through Community; `comments.create()` accepts typed attachment references.
331
332## 0.140.0 — 2026-11-10
333
334- `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.
335
336## 0.139.0 (2026-11-10)
337
338- `community.comments`: shared comment thread resolve/read/write/edit/delete and visibility methods, including ordered pages and paged replies.
339
340## 0.138.0 (2026-11-10)
341
342- **`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`.
343
344## 0.137.0 (2026-11-10)
345
346- 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 <site>" with `known: false`.
347
348## 0.136.0 — 2026-11-09
349
350- Network levels client: public person, batch and board reads, and service awards.
351
352## 0.135.0 (2026-11-10)
353
354- `ov.ai.files`: attach files through Media, list/read/remove them, and expose typed `ai.file@1` and conversational turn shapes.
355
356## 0.134.0 (2026-11-09)
357
358- `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`.
359
360
361## 0.133.0 (2026-11-09)
362
363- ov.blog.changelog({ service, kind, area, q, limit, before }): the network changelog's blog.changelog-entry@2 rows, newest first
364
365## 0.132.0 (2026-11-09)
366
367- `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.
368
369## 0.131.0 (2026-11-09)
370
371- openvibe-sdk/community: the Feed's types complete, with pagination guidance
372
373## 0.130.0 (2026-11-09)
374
375- openvibe-sdk/meme: ov.meme.templates(), .render() (PNG, GIF or MP4) and .get()
376
377## 0.129.0 — 2026-11-09
378
379- `openvibe-sdk/git` (G2): changes and review — `changes.list/get/create/comment/approve/merge/close/summary`,
380 `issues.list/get/create/labels/close`, `compare()`, `blame()`, `search()` and `reviewModel()`. Review requests
381 carry the expected head SHA, merge methods are checked, and mutations are POSTs with retries disabled.
382
383## 0.128.0 (2026-11-09)
384
385- ov.ai: tasks(), choices.get/set, run(task, input, {choice}) and admin.defaults(); runs.create accepts `task` and `choice`
386
387## 0.127.0 — 2026-11-09
388
389- `openvibe-sdk/git` (`ov.git`): repositories, trees, blobs, commits, branches, tags, releases, contributors,
390 insights and stars; `commits.create()` writes a branch and files without a clone; `tokens.create()` makes a
391 short-lived clone token; `clone(repo, { to: 'link' })` hands a clone to Link.
392
393## 0.126.0 (2026-11-09)
394
395- Add `openvibe-sdk/pics`: the public gallery and picture reads, private uploads with resumable chunks, publication, likes and albums on OpenVibe.Pics.
396
397## 0.125.0 (2026-11-09)
398
399- openvibe-sdk/blog: posts are written and read as Markdown or as blocks (publishing.block@1), ?format=blocks
400
401## 0.124.0 (2026-11-09)
402
403- OpenVibe Link: `openvibe-sdk/node` is `openvibe-sdk/link` (`ov.node` is `ov.link`), building
404 `platform.link-capabilities@1` and mapping a Link to its `platform.resource-offer@1` and
405 `platform.runtime-offer@1`. A link principal token is never a user token. The Contracts pin moves to
406 v0.372.0.
407
408## 0.123.0 (2026-11-09)
409
410- ov.exchange: keys (list, create, revoke), private usage, deployments, and web, image and icon search with fetch (the Exchange API platform)
411
412## 0.122.0 (2026-11-09)
413
414- dms.viewPreference() and setViewPreference(): a conversation's saved view, bubbles or room
415
416## 0.121.0 (2026-11-09)
417
418- ov.cam.settings.get/put: a person's saved camera choices (Cam #116)
419
420## 0.120.0 (2026-11-09)
421
422- createUploadClient(): resumable Media uploads in 8 MiB parts, three at a time, with a session reference to resume; ov.ui.progress(el)
423
424## 0.119.0 (2026-11-09)
425
426- openvibe-sdk/wallet: the wallet, Vibes, the store and items, through Billing and Inventory with the person's bearer and idempotency keys
427
428## 0.118.0 (2026-11-09)
429
430- openvibe-sdk/design: `components()` and `component(name)` read OpenVibe.Design's public design.component@1 registry
431 (a missing name is null, a bad one is refused before a request). They are on the same `createDesignClient` as the
432 icons; `accessToken` is now optional and only the icon API needs it.
433
434## 0.117.0 (2026-11-09)
435
436- ov.cam loads Cam's kit at its versioned URL and its new hash (Cam #116 changed the kit)
437
438## 0.116.0 (2026-11-09)
439
440- npm metadata (keywords, homepage, funding), the README's first screen and CITATION.cff
441
442## 0.115.0 (2026-11-09)
443
444- Tools discovery: ov.tools.popular() and ov.tools.trending()
445
446## 0.114.0 (2026-11-09)
447
448- Dock v2: Vibe conversations (ov.chat.vibe.conversations) and Actor skill runs (ov.actor.skills.run)
449
450## 0.113.0 — 2026-11-09
451
452- Add the Feed and status posts to `openvibe-sdk/community`: `feed({ kind, site, q, following, cursor, limit })` reads
453 Community's `GET /api/v1/feed` (one stream over Pulse from every OpenVibe site) and `posts.create({ text, pictures })`
454 posts up to four Media pictures with required alt text, in the Feed at once.
455
456## 0.112.0 — 2026-11-09
457
458- 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.
459
460## 0.111.0 — 2026-11-09
461
462- **`openvibe-sdk/shoes`: the Open.Shoes catalogue and sizing tools.**
463 - `createShoesClient(client)` (server-side; `./shoes` is browser-blocked) exposes public reads only:
464 `products.list({ kind, q, size })`, `products.get(id)`, `products.offers(id)`, `tools.laceLength({...})`
465 and `sizes.convert({ system, size, brand })`.
466
467## 0.110.0 — 2026-11-09
468
469- **`openvibe-sdk/design`: a typed client for the Design glyph API.**
470 - `createDesignClient({ accessToken, baseUrl })` returns `icons.search(q)`, `icons.draft({ name, sentence, styles, sourceSvg })`
471 and `icons.get(name, { style })` for the authenticated glyph draft, get and search calls. It is server-side only
472 (the `./design` subpath is browser-blocked) and takes a signed-in access token rather than a service token.
473 - Styles are checked against the set the API accepts (`thin`, `light`, `regular`, `bold`, `solid`, `duotone`).
474
475## 0.109.0 — 2026-11-09
476
477- Add Vibe compare methods to `openvibe-sdk/chat` for submitting answers and recording a user's preferred model.
478
479## 0.108.0 — 2026-11-09
480
481- Add daily quest reads, progress, step type listings and reviewable step contributions to `openvibe-sdk/quest`.
482
483## 0.107.0 — 2026-11-09
484
485- Add `ov.cam.open()` for the Cam recorder, with typed modes, options and file results across Node and browser exports.
486
487## 0.106.0 (2026-11-09)
488
489- Add `openvibe-sdk/home-feed` and `homeFeed` to the top-level and browser exports, with home layout, Today frame, and personal feed methods.
490
491## 0.105.0 (2026-11-09)
492
493- Add Report AI leaderboard, model, provider, comparison and picks reads, plus Exchange Arena start and vote methods.
494
495## 0.104.0 (2026-11-08)
496
497- Add `createAccountEmailsClient` to `openvibe-sdk/identity` for managing the signed-in person's verified Network email addresses.
498
499## 0.103.0 (2026-11-08)
500
501- Add `openvibe-sdk/credentials`: `createCredentialsClient` for a person's own provider credentials on OpenVibe.AI:
502 `list()` (metadata only: kind, provider, label, created, last used, expired), `add()` for an API key or a browser
503 session cookie, `remove()`, and `balance()` (that credential's fixed read-only balance readers: Anthropic Console
504 prepaid credits and current spend, OpenAI credit grants). A secret never comes back from any call.
505
506## 0.102.0 (2026-11-08)
507
508- Add `openvibe-sdk/trust`: `createTrustClient({ networkUrl, getToken })` with `.me()` (the caller's own signals, a
509 person's or a guest's token) and `.of(subject)` (a service token with `identity.trust.read`), the typed
510 `identity.trust@1` result for accounts and guests, and pure helpers (`isNew`, `atLeast`, the age-band order) for Node
511 and the browser bundle. Signals are age bands and aggregate counts only: never an IP, email or last-seen time.
512
513## 0.101.0 (2026-11-08)
514
515- Add bounded `locker/server.inspectEnvelopePrefix` validation against an actual stored ciphertext byte count.
516- Allow service-bound staged envelope receipts in share updates, with the existing recipient, nonce and atomic reseal checks.
517- Allow Locker public/private key export after `importPersonKeys`, so a recovered locker can upload and share using the same WebCrypto operations.
518
519## 0.100.0 (2026-11-08)
520
521- 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.
522
523## 0.99.0 (2026-11-08)
524
525- **`openvibe-sdk/events`: `outboxSchema()` indexes the rejected rows `rejected()` counts.** Every outbox made with the
526 helper now also gets `<table>_rejected` on `(rejected_at) WHERE rejected_at IS NOT NULL`. Before it, the count read
527 the whole outbox every time it was not cached (523 ms and 112 M buffers a day on Deals, 2026-11-08), because the
528 product had no index on `rejected_at`. A service that already has its outbox table must add the same statement in an
529 expand migration: the DDL is a migration helper, never run at boot, so `CREATE INDEX IF NOT EXISTS` in this helper
530 only reaches tables created after it.
531
532## 0.98.1 (2026-11-07)
533
534- `ai.batches.cancel` says what cancelling does: only the prompts that never had a run are cancelled, and
535 nothing is charged for them. A prompt that already has a run (the provider is working on it) goes on and
536 settles on its own path: it keeps its status, the answer says so, and nobody is told it was not charged.
537 The batch itself is cancelled only when nothing is left in flight. The same note is on `AiBatchItem.reason`.
538 No runtime change.
539
540
541## 0.98.0 (2026-11-07)
542
543- `ai.runs.create` (and the direct operations) may ask for the class the caller may wait in:
544 `{ latency: 'interactive' | 'background' | 'batch' }`, and for a batch run `{ deadlineHours: 1..72 }`
545 (how long the answer may take). The request carries `options.latency` and `options.deadline_hours`.
546 The class belongs to the workflow: a caller may ask for a slower class and never for a faster one, and a
547 faster ask is clamped by the service, not refused — the run answers with the class it really ran in
548 (`run.latency_class`), the tier it was priced on (`run.service_tier`) and, when it was clamped,
549 `run.latency_clamped`.
550- `ai.runs.waitFor` learns that a batch run may take hours: when the wait is over and the run is a batch
551 run, the pending run is returned (its status says where it stands) instead of an error. The answer comes
552 later, with the run's completion event (`ai.run.succeeded`) or the webhook the run was made with.
553- Add `ai.batches`: `create(items, { model, deadlineHours, idempotencyKey })`, `get(id)`, `list()` and
554 `cancel(id)` for OpenVibe.AI's `/api/v1/batches` — many prompts answered later, at half the retail
555 price. A cancelled batch cancels the items that had not run; nothing is charged for them.
556- Add the types `LatencyClass`, `ServiceTier`, `AiBatch`, `AiBatchItem` and `AiBatches`, and
557 `CLASSES` (the three classes) next to `TERMINAL`.
558
559
560## 0.97.1 (2026-11-07)
561
562- `types/menu.d.ts`: `MenuRequest` gives `url` and `method` the `| undefined` that `node:http`'s
563 `IncomingMessage` has, and `MenuResponse` no longer adds an index signature, so a plain
564 `IncomingMessage`/`ServerResponse` handler type-checks under `exactOptionalPropertyTypes`
565 (Games' `createMenuCardHandler`). No runtime change.
566
567## 0.97.0 (2026-11-07)
568
569- Add `openvibe-sdk/menu`: `menuCard({ id, site, title, kind, items, more, origin, ttlSeconds, updatedAt })`
570 builds one validated `frame.menu-card@1` with the house rules applied — plain text (markup and control
571 characters stripped) cut to the contract's lengths, at most six items (the contract's cap), https links on
572 the site's own origin or an `openvibe.*` host, and an optional item image (`{ src, w, h, alt }`) with the same
573 rule. A bad field throws a `TypeError` naming it; an empty `items` array is valid and Network skips the card.
574- Add `menuRoute(getCards, { maxAgeSeconds = 300, origin, log })`: the Express/node:http handler for
575 `GET /.well-known/openvibe-menu.json`. It calls `getCards()`, validates and caps the file at three cards, and
576 answers `Cache-Control: public, max-age=…` with CORS `*`; a build failure or an invalid card answers `[]`
577 (never a 500) and logs once. `MENU_PATH` is the route.
578- TypeScript types, README ("Put your content in the OpenVibe menu"), the API surface and the pack test follow;
579 the devDependency moves to openvibe-contracts v0.324.0, whose `frame.menu-card@1` tests validate every card
580 this module builds.
581
582## 0.96.0 (2026-11-07)
583
584- `openvibe-sdk/actor` gains `contacts`: `list()`, `presets()`, `get()`, `create()` (optionally from a preset),
585 `update()`, `remove()` and `setLevel()`, for a person's agents as contacts and their approval levels (plan §12
586 A-1). `contacts.list()` is `GET /api/v1/agents` with the person's own token, which now answers their contacts
587 (Vibe first), so `agents.list()` and the new `agents.catalog()` call the explicit public catalog
588 `GET /api/v1/agent-systems` and keep their old meaning. `tasks.create` types `agent_id` and `level`.
589## 0.95.0 (2026-11-07)
590
591- Add `openvibe-sdk/cam`: `defineOvCamera({ src })` injects OpenVibe.Cam's framework-free `<ov-camera>` element
592 once (default `https://openvibe.cam/embed/ov-camera.js`) and resolves to the class. It is browser-only by nature:
593 calling it in Node rejects instead of touching `document`. `OV_CAMERA_SRC` is the default URL.
594- Type the element: `OvCameraElement` (`start()`, `stop()`, `capture()`, `flip()`), `OvCameraAttributes` (facing,
595 filters, timer, mode, upload), `OvCameraCaptureDetail` / `OvCameraErrorDetail`, the two event types and the
596 `HTMLElementTagNameMap` entry. `upload="off"` is still the default: a capture is a `Blob` in the page until the
597 site sends it somewhere.
598
599## 0.94.0 (2026-11-07)
600
601- `openvibe-sdk/streams` gains S7: `schedule()`, `listSchedules()`, `cancelSchedule()` and `remindSchedule()` for
602 scheduled public agent streams; `steps.post()` and `postStep()` for any harness; `digest.read()` and `poll.open()` /
603 `poll.result()` for a Co-pilot's service token; and `onControl()` to honour pause, curtain, mute and end by polling
604 the stream's control log. The guards run before anything leaves the process (an `scd_…` id, a step's title and
605 status, two to four poll options, a 10–300 second poll).
606- Add `openvibe-sdk/reports`: `write()` posts a session report with the person's own token (private by default,
607 idempotent per source and task ref) and `read()` gets one the caller may see. The Report service redacts every text
608 field; the client never sends a raw transcript.
609- Add `examples/cli-agent-stream.js`: about 40 lines that start a stream, spawn any CLI command, post its steps and end
610 the stream with the command's result, using the same ticket path a Link desk uses.
611
612## 0.93.0 (2026-11-06)
613
614- `openvibe-sdk/exchange`: the seller side gains the **endpoint form**, so any OpenAI-compatible server can sell, not
615 only an OpenVibe Link machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with
616 no `machineId` — registers the server on OpenVibe.Exchange's public seller API
617 (`POST /api/v1/exchange/seller/offers`) with the same developer key the buyer side uses. The owner is the person the
618 key acts for (a developer app's project owner), never a body field, and the SDK refuses an endpoint that is not https
619 before anything is sent.
620 - The platform reaches that address only through its egress guard (https, a public DNS answer checked inside the
621 connection, never an IP literal, redirects judged again, a timeout and a response byte cap, no buyer identity in a
622 header) and verifies it exactly as a machine running Link (canaries, the model fingerprint, about 1% spot re-runs, the free
623 tier until verified, self-dealing earns 0). Its answers are labelled `community-endpoint:<id>` on the receipt.
624 - `seller.endpointOffers()` lists the caller's endpoint offers with their verification;
625 `seller.removeEndpointOffer(id)` removes one. `machineId` still selects the Network registry form.
626 - `seller.probe()` is unchanged, and `seller.offer` still probes the server's OpenAI model list first when `endpoint`
627 is given (`probe: false` turns that off).
628
629## 0.92.0 (2026-11-06)
630
631- Add `openvibe-sdk/chat-apps`: the three chat apps (Telegram, Discord, Slack) in one place, so a service that answers
632 a chat app reads them the same way (the plan of 2026-10-30, stage A4: "your agent in Telegram, Discord and Slack").
633 - The checks: `verifyTelegram()` (the `X-Telegram-Bot-Api-Secret-Token` header, compared in constant time),
634 `verifySlack()` (v0 over "v0:<timestamp>:<body>", the timestamp within five minutes), `verifyDiscord()` (Ed25519
635 over "<timestamp><body>" with the application's public key) and `verifyDelivery()` (the provider names its own
636 scheme; a provider this does not know is never verified). The code came from OpenVibe.Watch, which has checked
637 these deliveries since 2026-11-03 (Watch #194 and #195).
638 - The buttons: `signCallback()` and `verifyCallback()` carry a whole signed payload with its expiry;
639 `signButton()` and `verifyButton()` carry a 16-character row id in 44 bytes, inside Telegram's 64-byte
640 `callback_data`, and the caller checks who may press it, what it does, that it is used once and that it has
641 not expired against its own row. A token that was changed, replayed or is too old is refused.
642 - The messages: `parseTelegramUpdate()`, `parseDiscordInteraction()`, `parseDiscordMessage()`, `parseSlackEvent()`
643 and `parseSlackInteraction()` give each app's payload the same shape (who said it, where, what they said,
644 whether it was said to the bot, and the button that was pressed), never the app's own JSON and never a
645 message the bot itself sent. `splitText()` splits one answer the way the app accepts it, and `commandOf()`
646 reads a command and its arguments.
647
648## 0.91.0 (2026-11-06)
649
650- `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).
651 - `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.
652 - `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.
653 - `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).
654 - `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).
655 - 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.
656 - 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.
657
658## 0.90.0 (2026-11-06)
659
660- **Subject assertions** (ADR-003, the 2026-11-06 amendment; plan H1). A money call carries a Network-signed
661 proof that the person allowed the calling service to act for them, beside the name of the person.
662 - `requestSubjectAssertion({ userToken, audience, token | tokenClient, network | internalUrl })` asks Network to
663 sign the person's own access token into a 60-second assertion for one audience (`openvibe.billing`). The
664 person's token is the only thing that can produce one: without it Network signs nothing.
665 - `verifySubjectAssertion(assertion, { jwks, audience, act, seen })` checks the ES256 signature against
666 Network's JWKS (the EC keys, cached like the RS256 ones), the audience, the expiry and the jti; a forged,
667 expired, wrong-audience or wrong-service assertion is refused with a stable code
668 (`assertion.bad_signature`, `assertion.expired`, `assertion.wrong_audience`, `assertion.actor_mismatch`, …).
669 - `createSubjectAssertionVerifier({ jwks, audience })` remembers the jtis it has seen, so a replayed assertion is
670 refused inside the replay window.
671 - `subjectAssertionHeader(assertion)` gives the `x-ov-subject-assertion` header for the call.
672
673## 0.89.0 (2026-11-06)
674
675- 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).
676 - 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.
677 - 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).
678 - 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.
679
680## 0.88.0 (2026-11-06)
681
682- `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`.
683- 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`.
684- 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.
685- 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).
686- 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`.
687- 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.
688- 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.
689- `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`.
690- Pins openvibe-contracts v0.296.0 (was v0.292.0): `[email protected]`, `[email protected]_slot` and `[email protected]_fingerprint` are the shapes the locked sharing uses.
691
692## 0.87.0 (2026-11-06)
693
694- **The 1.0 core surface is frozen now.** `core`, `auth`, `events`, `db` and `media` are stable before 1.0:
695 removing or renaming one of their exports needs a major release. `docs/one-point-oh.json` keeps each name's tier,
696 `docs/api-surface.json` records every export, and `test/surface.test.js` now pins the exact export list of the five
697 entries, so a break there is a deliberate line in a diff rather than a surprise on a pin round.
698- **Removed the two `auth` leaving names**, planned for 1.0 in `docs/one-point-oh.md`: `createCodeVerifier` and
699 `pkceChallenge`. They were the two halves of `createPkcePair`, which stays frozen; no OpenVibers repository's `main`
700 imported them (checked 2026-11-06). Use `createPkcePair()` for both halves.
701- **`signDelivery` and `verifyDelivery` (the v1 signature) stay for now.** They are still the last leaving names, but
702 `signDelivery` is imported by the test helpers of OpenVibe.Deals, Examples, Reviews, Tips, Trade and VIP on `main`, so
703 they go once a pin round moves those onto `signDeliveryHeaders` / `signDeliveryV2`. `parseDelivery` still requires
704 v2 by default.
705
706## 0.86.0 (2026-11-06)
707
708- 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.
709 - `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.
710 - 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.
711 - A people slot names the recipient's key by its RFC 7638 thumbprint, and the reader checks it before it derives anything.
712 - The PBKDF2 iteration count is bounded on both sides: 600000 to 10000000, so a hostile header cannot make a reader work for hours.
713 - 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 `#`).
714 - 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.
715 - `wrapFor` and `rewrap`: adding a member to a people-locked item re-wraps the content key and leaves the content alone.
716- 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.
717- 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.
718- 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.
719- 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).
720- 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.
721- The fingerprint's two groups of four are joined by a middle dot, not a hyphen: a hyphen reads as one compound word.
722- Pins openvibe-contracts v0.292.0 (locker.envelope.v1 and the key directory API).
723
724## 0.85.0 (2026-11-05)
725
726- 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.
727- 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.
728- Pins openvibe-contracts v0.291.0 (share.v1). No service integrations or media migration are included in this core package.
729
730## 0.84.0 (2026-11-04)
731
732- **`openvibe-sdk/vibes`: Vibes Pay for developer apps** (MONEY.md, D12). A server creates a charge with its app
733 token (`billing.vibes.charge`), the person pays on OpenVibe's pay sheet in whole Vibes, and Billing moves them
734 once to the project's Vibes wallet.
735 - `createVibesClient({appToken | tokenProvider, billingUrl})` with `charges.create({amount, description, reference,
736 return_url})`, `charges.get(id)` and `charges.refund(id, {reason?})`. `amount` is a positive whole number of Vibes;
737 a float, zero or negative amount is a `TypeError` before any request. The app's `reference` is the
738 `Idempotency-Key`, so a retried create is the same charge. A token provider may be a function, an object with
739 `getToken()`/`invalidate()`, or one with `authHeaders()`; a 401 invalidates it and retries once with the same key.
740 problem+json becomes `VibesError` with `status`, `code`, `body` and `retryable`.
741 - `webhooks.verify(rawBody, signatureHeader, secret)` checks the raw body in constant time against
742 `X-OpenVibe-Signature-V2: t=<unix seconds>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, and rejects a timestamp
743 more than 300 s from the server's clock either way. The legacy `sha256=<hex>` (or bare hex) body-only form is
744 accepted. Bad input is `false`, never an exception. The same function is exported as `verifyWebhook`.
745## 0.83.0 (2026-11-03)
746
747- **`openvibe-sdk/placement`: `planAi()`, the AI Exchange face of the ONE planner** (plan-state 2026-10-22 §E,
748 G-P1/P7; decisions D1 and D5). Not a second router: `plan()` still answers `platform.placement-result@1` and is
749 untouched. **Off by default** — without `enabled: true` the planner answers nothing and no behaviour changes.
750 - **The hard gates, before any scoring.** Each one is a reason, never a weight: the exact artifact (an exact id
751 is never substituted), the commercial right (a paid managed route needs `OWN_DEPLOYMENT` or
752 `AUTHORIZED_WHOLESALE`), the privacy class and the trust tier, the capabilities (`tools`, `json`, `vision`,
753 `stream`), the context bound, the health, and the cost ceiling. A candidate with no rate is never assumed
754 free: it is rejected with `no rate: the cost is unknown`.
755 - **The objective is the cost of one success.** `cost_per_success = expected_cost / max(p_success, floor)`, so a
756 candidate that is cheaper per token but fails more often loses to one that is dearer per token and succeeds
757 more. `p_success` comes from optional quality evidence with a confidence: **sparse evidence is marked, not
758 trusted** (no confidence, or fewer than 20 samples → `sparse_evidence: true`, no confidence, and the probability
759 is pulled back to the prior), so a thin measurement can never dominate the ranking.
760 - **Money is integer nano-USD** (decision D1), never a float of dollars: `ceil(tokens * rate_nano_per_m / 1e6)`
761 charged once per token class (input, cached read, cache write, output) plus the per-request fee, and the cost
762 per success rounded up to a whole nano-USD. `openvibe-sdk/cost` gains `nanoUsd`, `chargeTokens`,
763 `expectedCostNano`, `costPerSuccessNano` and `formatNanoUsd`; the rates travel in the flat `ai.model@1`
764 `retail_rate` shape or the nested settlement shape.
765 - **The answer explains itself.** `explain.rejected` is `[{ id, reason }]` (as `ai.route-explain@1` names it) and
766 `explain.objective` says what was optimised for. Every reason is stripped of keys, subject ids, hosts, IPs,
767 URLs and paths: no secret and no internal host ever reaches the answer.
768 - Hysteresis and affinity stay the ones `plan()` has: `current` is kept unless another candidate is clearly
769 better (`minGain`), and `affinity` keeps a workload on the deployment it is warm on.
770
771## 0.82.0 (2026-11-02)
772
773- **`openvibe-sdk/events` outbox: `pending()` and `rejected()` keep their count** (`countsCacheMs`, 30 s when
774 `NODE_ENV` is production, off elsewhere), and callers at the same moment share one count. The metric gauges and
775 readiness ask on every scrape and probe, and `rejected()` has no index, so a large outbox was read whole each time:
776 115 ms on Deals, 56 ms on Trade, 49 ms on Sources, 45 ms on Search and 24 ms on Reviews (pg_stat_statements,
777 2026-11-02). Pass `countsCacheMs: 0` to count on every call.
778
779## 0.81.0 (2026-11-02)
780
781- **`openvibe-sdk/vision`: familiar faces (`face.familiar`), F1 of the L24 plan.** The camera owner's own Vision
782 library, reached with the owner's token:
783 - `list`, `create({ camera_ids, region, threshold })`, `get`, `update` and `remove` the library. The region picks
784 strict mode (Illinois, Texas, Washington, the EU, the UK; an unknown region too) or open mode.
785 - `enroll(libraryId, image, { name, subject })` names the crop the camera saw; in strict mode it answers a
786 `pending_consent` person with no template and a private `consent_request.url` until the person accepts.
787 - `match(libraryId, image)` matches only that library: a top match over its threshold, or `unknown person`.
788 - `people`, `patchPerson` (rename or mute), `removePerson`, `requestConsent`, `checkConsent`, `acceptConsent` and
789 `denyConsent`.
790 - The types in `types/vision.d.ts` say what the JSON says; no template, crop or other library is ever in an answer.
791- **`openvibe-sdk/meme`:** the client for OpenVibe.Meme's encyclopedia, trends, analyser, deep dives and generator.
792 - `createMemeClient(client, { baseUrl? })` (both browser and server; audience `openvibe.meme`, service id `meme`).
793 - `search({ scope, q, status, label, kind, sort, window, channel, limit, cursor })`, `entry(slug)`, `revisions(slug)`,
794 `trends({ window: 'day'|'week', limit })`, `analyze({ url | media_id })`, `research(topic)` (202, poll
795 `researchGet(id)`), `saveResearch(id)` and `generate({ template | template_slug | topic, lines? })`.
796 - `analyze` refuses locally unless exactly one of `url`/`media_id` is given, and `generate` refuses with neither a
797 template nor a topic; no request is sent for either. Types are in `types/meme.d.ts`; the browser bundle includes
798 the client. The `meme` namespace is on `openvibe-sdk` and in the browser bundle, and `openvibe-sdk/meme`
799 (`createMemeClient`) is in docs/api-surface.json.
800- **`openvibe-sdk/app-signin`: "Sign in with OpenVibe" for developer apps, in one call.**
801 - `createAppSignIn({ clientId, clientSecret, redirectUri, networkUrl, audience, scope, session })` gives a router
802 with `/login`, `/callback`, `/logout`, `/me` and `/health`, built on the SDK's own auth helpers
803 (authorization code + PKCE, the state check): nothing here re-implements OAuth.
804 - The same recovery as `openvibe-sdk/sso` 0.73.0: a lost or expired flow and a code the Network no longer accepts
805 start the sign-in once more by themselves; the second failure shows the same plain page (`failurePage`, now
806 exported by `openvibe-sdk/sso`), with "Try again" and the way back. Neither ever sets a session.
807 - The same client check: `GET /health` and `checkClient()` answer whether the Network accepts the app's
808 credentials (a code that is never issued; cached 10 minutes; 503 only when they are refused).
809 - The app token is verified offline against the Network's JWKS and only trusted when it was minted for THIS app
810 (`sub = app:<clientId>`), before `on_behalf_of` names the person.
811 - The session: your own store (`session: { get, set, clear }`) or, by default, an in-memory store with an opaque
812 httpOnly cookie. The secret, the PKCE verifier and the token never reach the browser and never appear in a log.
813- **`openvibe-sdk/live`: the rest of the Go live screen.** `live.slots.history(id)` (the slot's past streams),
814 `live.slots.quality(id, { resolution, fps, bitrate })` and `live.slots.titleIdeas(id, { weekday, hour, title })` (the
815 card's ✨ Suggest: three titles for the next stream). With these, an app does everything Live's Go live page does
816 (https://openvibe.live/docs/streams-api).
817
818## 0.80.0 (2026-11-01)
819
820- **`openvibe-sdk/explicit`: nudity is art.** The owner, 2026-10-31: "naked and nude is fine… stop being against art on
821 my platform." `isExplicit()` now gates what OpenVibe.Live's own rule gates (Live#469), word for word.
822 - **Still gated:** sex acts and pornography (sexual acts or activity, having sex, porn, masturbation, intercourse,
823 oral sex, penetration, pornographic or sexually explicit content), and any text that puts a child and nudity
824 together.
825 - **No longer gated:** nudity and body words on their own. A nude figure-drawing class publishes; "a child is shown
826 naked" never does.
827
828## 0.79.0 (2026-10-31)
829
830- **`openvibe-sdk/machines`: the network's machines hub** (plan L34, step M2; Contracts 0.258.0). A person's machines
831 are the network's: Bot stays the registry underneath, and OpenVibe.Network fronts it on openvibe.link/machines for
832 the person and for the apps the person consented to.
833 - `createMachines({ networkUrl, token, fetch, timeoutMs })`: `token` returns the caller's own bearer token — the
834 person's session token, or an app's token holding `network.machines.read` or `network.machines.manage` with the
835 person's consent.
836 - `list()` → `{ machines }`, `get(id)` → `{ machine }`, `catalogue()` → `{ roles }` (what a machine may do for the
837 network), `pair()` → `{ pairing }` (a one-time code and the command to run), `rename(id, name)` → `{ machine }`,
838 `roles(id, roles)` → `{ machine }` (the whole set, not a change), `revoke(id)` → `{ machine }`.
839 - A refusal throws a `MachinesError` with the hub's own `status` and `code` (`capability.denied`,
840 `capability.owner_denied`, `registry.unknown_role`, a 404 for a machine that is not the caller's). A hub that does
841 not answer is a `machines.timeout`, never a hang.
842 - The machines' own capabilities stay where they are: `openvibe-sdk/bot`'s `machines.desk()` and `machines.chat()`
843 talk to Bot itself and are unchanged.
844
845## 0.78.0 (2026-10-31)
846
847- **`openvibe-sdk/consent`:** the client for the L24 consent registry. Nothing a person makes is off-limits to AI or
848 data use by design; each use is off until that person turns it on in OpenVibe.Network. A service asks this check
849 before it uses a person's content or likeness.
850 - `createConsent({ networkUrl, token, ttlMs = 60_000, fetch, log })`: `token` returns a service token holding
851 `network.consent.read`. `allowed(subject, purpose, { kind, client, feature })` → a boolean;
852 `filter(subjects, purpose, opts)` → the allowed subset, in input order, in batches of 500;
853 `onChange(event)` drops a subject's answers for a purpose on `network.consent.changed` (wire the payload or the
854 envelope); `mock(map)` answers from `{ subject: ['purpose'] }` without the network.
855 - **Fails closed:** a network error, the 2 s timeout, a non-200, a malformed answer or a token that cannot be
856 minted is false for everyone asked. A failure is never cached as a yes; it logs at most once a minute.
857 - An answer is cached for `ttlMs` (60 s), per subject, purpose, kind, client and feature; an answer in flight when
858 a change arrives is not stored, so a revocation cannot be overwritten.
859 - Server-side (`browser: null`): the token is a service token. Types are in `types/consent.d.ts`, the dataset-build
860 example in `docs/consent.md`.
861
862## 0.77.0 (2026-10-31)
863
864- **AI text may describe people** (the owner, 2026-10-31: "there should be no reason it can't describe people … remove
865 all that crap"). `openvibe-sdk/no-looks` is deprecated. `noLooks`, `noLooksTags` and `hasLooks` return what they are
866 given, and `RULES_VERSION` is `'off'`, so a service that still calls them keeps working until its next release
867 removes the calls.
868- **`openvibe-sdk/explicit`:** `isExplicit(...texts)`, the rule that stays. AI text that says a scene shows sexual
869 content is not published. `no-looks` re-exports it.
870
871## 0.76.0 (2026-10-30)
872
873- **`openvibe-sdk/placement`: placement v1 — quality, the person's rules, learning and a reason (agent plan A1).**
874 - `place(requirements, offers, ctx)` is `plan()` with the plan's road: the person's rules and ownership, a quality
875 floor (default 0.5) with a fallback, the success estimate, cost per success, escalation and one plain sentence
876 saying why. It returns `{ result, line, ranked, rejected, fallback }`; `result` is exactly what `plan()` returns
877 for the offers left in, validated as today.
878 - `learn(outcomes, {person, key, kind, ok, at})` and `successOf(outcomes, {person, key, kind, prior, now})` keep
879 and read plain counts with a 14-day half-life, a Beta mean, Bench priors, the global weight `min(1, 20/nG)` over
880 **everyone else's** counts (the person's rows are not counted twice) and the dotted-kind fallback.
881 `escalate(previous, failure, requirements, offers, ctx)` handles quality, busy, offline, refused, budget and
882 timeout, and asks the person after two escalations. It learns the failed try exactly once and returns the new
883 counts as `outcomes`; the caller persists that set and must not record the same failure again.
884 `optionKey(offer)` and `lineFor(facts)` are exported too.
885 - On the person's own free machine the line claims only what the step asked for: `harness:tools` (the harness's
886 own tools, including the signed-in CLI) says "your sign-in", and `harness:host-access` (the person's machine and
887 its files) says "your files". With neither it says "your own machine".
888 - `plan()` takes an optional `success: { [offer_id]: p }`: when present the cost part of the score is
889 `estimated_cost_usd / p`, while `estimated_cost_usd` in the result stays the raw cost. When absent, scoring is
890 unchanged.
891 - **Behaviour fix: `user-owned` is never a default.** When `requirements.trust` is absent, `plan()` now uses
892 `DEFAULT_TRUST` (`first-party`, `partner`, `community`, `external`). The contract
893 (`platform.workload-requirements@1`) says exactly that: "when absent, first-party, partner, community and
894 external. user-owned is never a default: a user-owned offer is eligible only when this list names it". A
895 stranger's machine must never be a default. `TRUST_ORDER` keeps all five classes as the preference order;
896 `DEFAULT_TRUST` is the four-class default. `place()` also requires a user-owned offer's `owner` to be the person
897 of the step.
898 - **Streaming machines.** A step that asks for a GPU or 2 or more cores excludes an offer whose
899 `health.streaming` is `true`, with the reason "it is streaming". The field is not in
900 `platform.resource-offer@1` yet (Link reports `screen.state` in its own status, Bot's offer does not carry it),
901 so this release fixes the name Link can start sending; a top-level `offer.streaming` is tolerated too.
902 - README and [docs/placement.md](docs/placement.md): the API, the rules, the estimate, escalation and the line
903 templates.
904
905All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
906release may change an API and says so here.
907
908## 0.75.0 (2026-10-30)
909
910- **`openvibe-sdk/redact`: one secret scrubber for Link and AI (Memory M2).**
911 - `redact(text)` → `{ text, count }` ports OpenVibe Link's `internal/memory/redact.go` (Node #87): the same shapes,
912 the whole-text pass with a sentinel before the per-item pass, the unterminated private key block, Luhn card
913 numbers, and the deliberately wide environment-assignment rule. `onlySecret(text)` says an item was nothing but a
914 secret, so AI refuses it.
915 - `test/redact-corpus.json` holds every case of Node's `TestRedactEveryShape`, the split and unterminated key
916 blocks, the `.env` block and the harmless texts that must not change. Node copies the file and runs the same
917 cases against its Go scrubber, so the two ends share one list and one corpus.
918 - Pure functions, no dependencies: exported for Node and the browser, with ESM and TypeScript entries.
919
920All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
921release may change an API and says so here.
922
923## 0.74.0 (2026-10-30)
924
925- **`openvibe-sdk/account-data`: telemetry leaves with the person, and a guard finds a table nobody covers (ADR-033).**
926 - `createAccountData` handles `telemetry_samples` by itself when the table exists: the person's samples are exported
927 as `telemetry.json` (`at`, `service`, `operation`, `resource`, `status`, `latency_ms`), and a deletion anonymizes
928 them (`subject` set to NULL, the sample kept) instead of removing them. A service that lists the table itself
929 keeps its own entry.
930 - `uncovered(db, {tables, taskKeyed, exempt})` lists every table in the current schema with a person-shaped column
931 (`subject`, `owner`, `created_by`, `user_id`, `person`) that the table map, the telemetry handling, a task-keyed
932 list or a reasoned exemption does not cover. A service's test asserts the list is empty, so a new table fails CI
933 until it is covered or exempted with a reason.
934
935## 0.73.0 (2026-10-30)
936
937- **`openvibe-sdk/sso`: a sign-in that recovers, explains itself and can be checked.**
938 - A lost or expired flow (no or another state cookie) and a code the Network no longer accepts (`invalid_grant`) start
939 the sign-in once more by themselves. A two-minute `ov_oauth_retry` cookie keeps it from looping, and neither ever
940 sets a session.
941 - A second failure, a client the Network refuses (`invalid_client`) and a Network that does not answer show a small,
942 self-contained page in plain words, with "Try again" and "Back to <site>", in place of a bare line of text.
943 - A refused client is logged as a configuration error that names `OV_OAUTH_CLIENT_ID` / `OV_OAUTH_CLIENT_SECRET`.
944 - `GET /auth/health` and `checkClient()`: a token request with a code that is never issued. `invalid_grant` means the
945 client is fine; `invalid_client` that its id or secret is wrong (503). The answer is cached 10 minutes.
946 - Why: OpenVibe.Cam ran for a day on the placeholder secret from its `.env.example`. Every sign-in ended on "The
947 Network refused the sign-in code", and nothing checked.
948 - `siteName` option for the pages.
949 - `legacyCookies: { access, refresh }` moves a site off its own sign-in code without signing anyone out. Its old
950 cookies are read as the session until the next sign-in or refresh writes `ov_token`/`ov_refresh` and clears them;
951 logout clears both. `httpOnlyAccess: true` keeps the access token httpOnly, as those sites did (their navbar asks
952 `/auth/me`).
953
954## 0.72.0 (2026-10-29)
955
956- **`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.
957
958## 0.71.0 (2026-10-28)
959
960- **`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`.
961
962## 0.70.0 (2026-10-28)
963
964- **`openvibe-sdk/search`: the Search Fabric** (Contracts 0.225.0 `search.fabric.query`; plan 2026-10-28 §4.5). One
965 query over several scopes, routed the cheapest way that answers.
966 - **`search.fabric(text, { scopes, freshness, depth, answer, limit, cursor, owner, type, lang, site })`** returns
967 `search.fabric-result@1`:
968 - `results`: each has a scope and a kind (a document in the index, or a web page with its provider, URL and
969 dates);
970 - `groups`, `citations`, and `route.explain` (what each step did and what it cost).
971 - **Scopes:**
972 - `network` and `mine` (the person's own; a service passes `actingSubject`);
973 - `web`: OpenVibe's open index first, then DeepSeek for a signed-in caller or a service, then Wikipedia,
974 cached;
975 - `news`, `images`, `video`, `products`, `people`, `places` and `code`.
976 - `search.query(text, { scopes })` is the same call; without `scopes`, `query()` is the index search as before.
977 - `search.providers()` lists the web ladder's `search.provider-offer@1` items.
978 - Reads use GET, so the client may retry them.
979 - Types: `FabricOptions`, `FabricAnswer`, `FabricResult`, `FabricExplainStep`, `ProviderOffer`; `fabricOf()` builds
980 the query string.
981
982## 0.69.0 (2026-10-28)
983
984- **`openvibe-sdk/tools`: domain history** (OpenVibe.Tools' `/api/v1/domains`, 2026-10-28). What the WHOIS and RDAP
985 tools saw of a domain over time, public registration facts only (registrar, dates, status, name servers, DNSSEC;
986 never contact details).
987 - `tools.domains.history('example.com')` gives its snapshots and what changed when; it is null when nothing was kept.
988 - `tools.domains.lists({ limit })` gives the most looked up domains and those of them expiring soon (each looked up
989 five times or more).
990 - Both read without a token; a malformed name is refused in the client. Types: `DomainHistory`, `DomainLists`,
991 `DomainFacts`.
992
993## 0.68.0 (2026-10-28)
994
995- **`openvibe-sdk/food`: food boxes** (OpenVibe.Food's `/boxes`, 2026-10-28). Every meal kit, ready meal and food box
996 side by side: `food.boxes.list({ kind, diet, q })`, `.get(slug)` (with the box's codes on OpenVibe.Coupons and its
997 score on OpenVibe.Reviews, null when there are none) and `.cost(slug, { people, meals, weeks })` (what its stated price
998 comes to).
999 - Each box's facts were read on the service's own pages, with the page and the day (`sources`, `checked`).
1000 - Types: `FoodBox`, `FoodBoxCost`, `BoxKind`.
1001
1002## 0.67.0 (2026-10-27)
1003
1004- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe Link v0.27.0's ai plugin).
1005 - `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1
1006 kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`
1007 with code `bot.ai_nack`.
1008 - `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
1009- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and
1010 `computer.ai_speed` (how fast the last model answered there, Link v0.29.0); `MachineChatInput` and `MachineChatResult`;
1011 `MachineCommandInput.kind` takes `ai`.
1012
1013## 0.66.0 (2026-10-27)
1014
1015- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,
1016 `.computer(id)`, `.command(id, { kind, value })` (bot.command@1's computer kinds) and `.deskTurn(id, desk)`.
1017- **`bot.machines.desk(id, { name?, kind?, profile? })`** is a desk of your own on a machine: a browser alone or a whole
1018 screen, with its own files and sign-ins (OpenVibe Link v0.19.0 to v0.26.0).
1019 - `start`, `stop` and `status`.
1020 - `browser`, `screen`, `input` and `exec` on that desk only, never on the person's own screen. `exec` takes
1021 `detach` for an app that keeps running until the desk stops.
1022 - `turn()` says whether a person is using the desk now.
1023 - Each method answers the machine's `data`. A command the machine does not do throws an `OpenVibeError` with code
1024 `bot.desk_nack`.
1025
1026## 0.65.0 (2026-10-26)
1027
1028- **`openvibe-sdk/sso`: switch account and use another account** (the owner, 2026-10-26: sign-in "like the Google
1029 sign-in does … open to SDK and API"). `GET /auth/login?prompt=select_account` shows OpenVibe.Network's account
1030 chooser, `?prompt=login` its sign-in form, and `?login_hint=<username>` names the account the chooser continues as when
1031 the browser has it (OpenID Connect; Network's docs/sign-in.md). `?silent=1` stays `prompt=none`; anything else is
1032 dropped.
1033- **`openvibe-sdk/chat`: Vibe, support inboxes and feeds** (#78; the owner, 2026-10-24: OpenVibe Chat "fully modular
1034 and SDK and API"). `createChatClient` gains `vibe.*` (a person's chats with Vibe), `inboxes.*` (the team's side of a
1035 support inbox: conversations, replies, members, verified visitors, saved replies, actions, assign, translate) and
1036 `feeds.*` (the owner's side of a feed; `view` and `onLive` send no token). `createInboxVisitor(client, { inbox, token,
1037 user })` is a visitor's side with no account and no cookies (visit, send, messages, handoff, rate, sendPicture), and
1038 `createFeedPublisher(client, { feed, key })` publishes vibe coding events with a feed's key (`claudeCode(hookJson)`).
1039- **`openvibe-sdk/themes`** (#80, #81; the owner, 2026-10-25: the theme system in the SDK and API). `createThemesClient`
1040 reads and sets the account's theme on Network (catalog with seasons, active, set, setDisplay, seasons, submissions,
1041 import), and `followTheme(fn)` keeps a page, canvas or game in step with the theme someone picked on any OpenVibe site
1042 (`readTheme()`: id, mode, tokens, display, season). `DISPLAY_OPTIONS.scheme` is `manual | auto` (Shared 4.12's Light /
1043 Dark / Auto), and each season says its light half.
1044- **`openvibe-sdk/play`: `titles.remove`** (#82) deletes a sandbox title you manage (OpenVibe.Play#42; a developer's
1045 sandbox app registers up to 3 `sandbox-` titles since Play#41). A production title is kept (409 `play.title_kept`).
1046- `openvibe-contracts` v0.213.0 (#79).
1047
1048All additions: nothing exported before changes.
1049
1050## 0.64.0 (2026-10-25)
1051
1052- **`parseDelivery` requires a v2 signature by default.** A delivery without `X-OpenVibe-Signature-V2` is refused
1053 unless the caller passes `{ requireV2: false }`, which is deprecated. A v1 signature (`X-OpenVibe-Signature`) signs no
1054 time, so a captured delivery verified forever. OpenVibe.Events sends only v2, and every production receiver
1055 already passed `requireV2: true`, so nothing that runs changes. A test that signs with `signDelivery` and parses
1056 without options now gets `null`: sign with `signDeliveryHeaders` instead.
1057- **`signDelivery` and `verifyDelivery` are deprecated** and leave in 1.0, with `createCodeVerifier` and
1058 `pkceChallenge` (the halves of `createPkcePair`, unused anywhere).
1059- **What 1.0 freezes, name by name.** [docs/one-point-oh.md](docs/one-point-oh.md) puts each name of the five core
1060 entries in a tier (frozen, platform or leaving), with the reasons and how many repositories use it.
1061 `docs/one-point-oh.json` holds the lists. `test/surface.test.js` fails when a name there has no tier, and
1062 `scripts/surface.js --check` marks a breaking change with its tier.
1063- `types/events.d.ts`: `signDeliveryHeaders` returns the timestamp and the v2 header. The type had also listed a v1
1064 header that the function never set.
1065- **no-looks knows nudity words** (`naked`, `half-naked`, `nude`, `undressed` and their variants), and `hasLooks` skips
1066 character names (Iron Man, Pac-Man, Spider-Man), as `noLooks` already did. Live's AI moments
1067 job titled a clip "A Naked Man Lying On Their Back With Legs" on Live's home (2026-10-24), and a Community paste
1068 read "Noteworthy Half-Naked Moment". `hasLooks` now rejects both, and `noLooks` repairs sentences as it does for
1069 "shirtless". `RULES_VERSION` changes, so services that repair stored rows run their repair again.
1070- **no-looks knows clothes below the waist after "in"** (`shorts`, `jeans`, `pants`, `trousers`, `leggings`, `skirts`,
1071 `sweatpants`, `swimwear` and the like): "The streamer in shorts kneels on grass…" had survived every one of
1072 Community's repairs since April 2026. A shop word after it ("in the pants department") or a pocket keeps the phrase.
1073 Team kits and uniforms are not touched.
1074- **`openvibe-sdk/front-page`** (new): `frontPageOk(text)` says whether a title or line may lead a front page (no
1075 built-in slur category, no strong profanity in five languages, no hate topic), and `containsCoreSlur(text)` is the
1076 slur check alone. The categories are OpenVibe.Chat's built-in ones, ported unchanged. An anonymous screenshot titled
1077 with a slur sat in OpenVibe.Community's "Most viewed" (2026-10-24), and Live had its own copy of the rule for its
1078 quote strip; front pages now share one.
1079- **`no-looks` gains `isExplicit(...texts)`**: true when AI text says a scene shows sexual content (body parts, sex
1080 acts, pornography, nudity). The looks rules only remove an adjective ("a man … reaching toward their genitals"), so a
1081 service asks before it publishes AI text at all. Live's AI auto-clip 483 went public on openvibe.media with such a
1082 description on 2026-10-23. Live#344 keeps its own copy of the pattern until it pins this release. `RULES_VERSION`
1083 includes it.
1084- **A JWKS client with no key retries by itself** on its backoff (1 s up to 5 min) until a fetch succeeds, instead of
1085 only when a token next needs checking. On 2026-10-23, Trade and the Tools gateway started before Network after the
1086 reboot and reported "no key" on `/status` for six hours, until a restart; their first sign-in would have waited for
1087 the fetch.
1088
1089## 0.63.0 (2026-10-24)
1090
1091- **The public surface is pinned.** `docs/api-surface.json` lists every name each of the 66 entries exports and what it
1092 is; `test/surface.test.js` fails when they disagree, and `node scripts/surface.js --check` names each change as
1093 breaking (a name gone, a kind changed, a constant's key gone) or added. The first step toward 1.0's freeze of
1094 `core`, `auth`, `events`, `db` and `media`. Nothing in the package's behaviour changes.
1095- **Outbox relays pause on a 429.** `createPgOutbox` (and so `createServiceOutbox` and the usage relay) used to hit
1096 Events' per-actor limit again on every tick after a 429, because the rows it claims next are under the same limit.
1097 Search's re-index bursts were refused 935 times on 2026-10-23, and every event was delivered only on a retry.
1098 - A 429 now pauses the whole relay until the limit's window reopens: its `retry_after_seconds`, else a minute, at
1099 most an hour.
1100 - Passes before then send nothing. The refused rows back off as before, and a 429 never rejects a row.
1101 - Nothing changes for a service that stays under its limit.
1102- **`createServiceOutbox` prunes sent rows.** Rows sent more than `pruneAfterMs` ago (default 7 days; `false` keeps
1103 them) are deleted every 6 hours while the relay runs, and once a minute after `start()`; `out.prune()` runs it now.
1104 - Services that never called `outbox.prune()` kept every event they ever sent: on 2026-10-23, Work had 52,130 rows
1105 (119 MB) since 10-10, and Trade, Deals and Help since September or early October.
1106 - A service that already prunes itself is unaffected (a second prune finds nothing).
1107
1108## 0.62.0 (2026-10-23)
1109
1110- **`openvibe-sdk/no-looks`: pronouns.** A pronoun is a guessed gender too, so the 27 Community summaries repaired on
1111 10-22 that still said "he looks at the camera" or "on his device" are covered. Outside quotes:
1112 - he and she become "the person" (the verb stays as written: "he looks" → "the person looks"; he'll → the person
1113 will, he'd → the person would);
1114 - his and her before a word become "their"; him, and her after a verb, a preposition or at the end, become "them";
1115 - himself and herself become "themself", hers and a final his "theirs".
1116 Quoted speech stays word for word. `RULES_VERSION` changes, so services that repair stored rows run their repair
1117 once more.
1118- **`openvibe-sdk/live`: where a slot publishes.** `live.slots.profile(slotId)` reads a slot's settings and ingest
1119 (`GET /api/streams/managed/:id/profile`, owner): its stream key, Live's WHIP origin and RTMP URL, or, for a slot
1120 OpenRestream ingests, OpenRestream's own WHIP and RTMP URLs and no key. `live.whip.forSlot(slotId)` turns that into
1121 `{ url, key, authority }` to pass to `live.whip.publish()`, so going live from code no longer builds the WHIP URL by
1122 hand (OpenVibe.Examples' go-live example, plan G2).
1123
1124## 0.61.0 (2026-10-22)
1125
1126- **`openvibe-sdk/no-looks`**: the owner's rule that public AI text never describes a person, as one shared filter.
1127 `noLooks(text)`, `noLooksTags(tags)`, `hasLooks(title)` and `RULES_VERSION`, moved from OpenVibe.Live
1128 (Live#298–#302) with its test corpus, so Community, Search, MediaHub and every service that stores public AI text
1129 run the same rules. Pure and dependency-free (Node and browsers).
1130
1131## 0.60.0 (2026-10-20)
1132
1133- **`openvibe-sdk/host`** and **`openvibe-sdk/news`**: the public reads of the last two products without a module.
1134 Host: the hosting catalog (providers, plans, promos, community offers, domain endings with registrar prices, a
1135 name's availability, YABS benchmarks), with Host's own filters (any other key is refused). News: topics and the
1136 published and retracted stories with their sources, timeline and perspectives. Both are sent without a token;
1137 editing stays with each site's editors.
1138
1139## 0.59.0 (2026-10-20)
1140
1141- **`openvibe-sdk/space`** (`createSpaceClient`): OpenVibe.Space for apps and CI, public in openvibe-contracts 0.209.0.
1142 Deploy a static site as a tar.gz archive or files with their paths (activate at once, or keep a preview), activate,
1143 roll back, read a deploy's log; manage projects (members as deployer, maintainer or owner), sites, their serving
1144 settings and Git source, and custom domains. An app is a principal of its own in Space and reaches only the
1145 projects a person added it to. Uploads and creations are never retried on their own.
1146
1147## 0.58.0 (2026-10-20)
1148
1149- **`openvibe-sdk/coupons`**, **`openvibe-sdk/deals`** and **`openvibe-sdk/trade`**: what a person does for themselves
1150 on those sites, public in openvibe-contracts 0.208.0. Coupons: which store a site is, its codes, submitting a code and
1151 reporting whether one worked (what a browser extension needs). Deals: hot and new offers, posting, price
1152 observations, votes, flags and watches. Trade: instruments with their sourced observations and filings, and the
1153 person's watchlists and alert rules. Moderation stays with each site. Writes that make a new row each time are
1154 never retried on their own.
1155
1156## 0.57.0 (2026-10-20)
1157
1158- **`openvibe-sdk/reviews`** (`createReviewsClient`): OpenVibe.Reviews for apps, whose reads became public in
1159 openvibe-contracts 0.207.0: resolve a business, product or place from its identifiers (a match or candidates), list
1160 and read entities with their aggregate, signals, summary and history, and send a correction to the editors for the
1161 person (never retried on its own).
1162- **`openvibe-sdk/wiki`** (`createWikiClient`): OpenVibe.Wiki's reads: search, spaces, and pages with their revisions,
1163 diffs and citations, as the person may read them (without one, public pages only).
1164
1165## 0.56.0 (2026-10-20)
1166
1167- **`openvibe-sdk/blog`** (`createBlogClient`): OpenVibe.Blog, the first product whose API only its own site could use
1168 (its capabilities became public in openvibe-contracts 0.206.0). Read a blog's published posts and feed; for the
1169 person who authorized the app, create drafts, edit (a new revision, `expected_revision` guards a race), publish,
1170 unpublish, schedule, revert and delete posts. Blog acts for an app only as its `on_behalf_of` person. Creating a
1171 post or reverting makes a new one each time and is never retried on its own.
1172
1173## 0.55.0 (2026-10-20)
1174
1175- Six more products through the SDK, each a server module over the routes its capabilities name in Contracts:
1176 - **`openvibe-sdk/food`** (`createFoodClient`): places to get food, the food list with prices, meal plans, a
1177 person's pantry with its "what can I make" matches, and AI recipe ideas.
1178 - **`openvibe-sdk/actor`** (`createActorClient`): tasks for AI agent systems (create, list, get, cancel, and
1179 `tasks.events`, the SSE stream until the task ends), the public agent list and dry-run routing, and a person's
1180 own agents with their runs and memory.
1181 - **`openvibe-sdk/run`** (`createRunClient`): jobs on worker nodes (submit with an Idempotency-Key, list, get,
1182 cancel, `jobs.events` until `end`, a browser stream ticket), `forProject` for X-OV-Project.
1183 - **`openvibe-sdk/video`** (`createVideoClient`): OpenVibe.Video's playlists of recordings and clips, and a
1184 person's public playlists.
1185 - **`openvibe-sdk/work`** (`createWorkClient`): the job board's search and sources, saved searches, tracked
1186 applications and the agent's help with one.
1187 - **`openvibe-sdk/zone`** (`createZoneClient`): a project's object zones and their usage.
1188- A write that makes something new each time (a plan, recipe ideas, an agent, a run, a memory note, a playlist or an
1189 item in it, a saved search, an application without a listing) is never retried on its own. Keyed writes (Run's
1190 submit, Zone's create, an Actor task with an idempotency_key, an application for a listing) and changes are.
1191
1192## 0.54.0 (2026-10-20)
1193
1194- **`openvibe-sdk/tips`**: OpenVibe.Tips through the SDK (it had 19 capabilities and no module). `createTipsClient(client)`
1195 covers its `/api/v1`: a creator's tip page (`profiles`), tipping (`tip` → `{ interaction, checkout_url,
1196 checkout_ref }`) and paid messages, TTS, media and sound requests (`send.*`), receipts and external tips
1197 (`interactions`), `goals`, `moderation` (queue, log, moderators), `overlays` (tokens, configs, a test alert) and the
1198 supporter's own `me.export` / `erase`. Tips replays an Idempotency-Key once, so every write carries one and a
1199 retried tip is safe.
1200
1201## 0.53.0 (2026-10-20)
1202
1203- **`openvibe-sdk/inventory`**: OpenVibe.Inventory through the SDK, as the owner asked of it ("cross site network wide
1204 shared multi purpose modular system … through sdk / api"). `createInventoryClient(client)` covers its `/api/v1`: the
1205 public reads (`kinds`, `definitions`, a person's `items` and `equipped`, `equipped([subjects])` for up to 100 at once),
1206 the person's own (`me.items`, `me.equip`, `instance_id: null` clears a slot), and an issuer's (`definitions.create` /
1207 `update`, `grants.create` → `{ instance, created }`, `instances.consume` / `revoke`). A grant is retried safely: its
1208 body's `idempotency_key` is what Inventory dedupes on. `forPerson(usr_…)` is the same client sending `X-OV-Subject`
1209 for a service acting for a person. Ids are checked against Inventory's own rules before any call.
1210
1211## 0.52.0 (2026-10-20)
1212
1213- **`openvibe-sdk/live`**: OpenVibe.Live in the SDK, its flagship product had no module. `createLiveClient(client)`
1214 covers the routes documented at openvibe.live/documentation: who is live (`streams.list`, `recentlyOnline`), a
1215 channel (`channels.get`, `url`, `toggleFollow`), slots (`slots.create` … `regenerateKey`), going live
1216 (`goLive(slotId, opts)` → `{ stream, endpoint }`), the session (`update`, `heartbeat`, `endpoint`, `end`), the content
1217 feeds (`content.feed`, `content.moments`), and WHIP publishing (`whip.url`, `whip.publish` → `{ answer, location }`,
1218 `whip.end`). Signed calls carry the person's Network token; `/whip` gets only the stream key, never the client's
1219 token. Writes carry no idempotency key and are never retried (a second go-live is a second session).
1220
1221## 0.51.1 (2026-10-17)
1222
1223- `openvibe-sdk/sso`: `GET /auth/me` for a signed-out visitor (or an expired, foreign or non-session token) answers
1224 `200 { user: null }` instead of 401, as Blog, Community and Wiki do. The shared navbar treats both the same; the 401
1225 showed as a failed request in every signed-out visitor's console.
1226
1227## 0.51.0 (2026-10-17)
1228
1229- `openvibe-sdk/commerce`: subscription tiers priced by OpenVibe.Billing (openvibe-contracts 0.201.0). `createIntent`
1230 (kind subscription) and `subscribeWithCredit` take an optional `tier`; `listTiers(creator)` reads a creator's tier
1231 prices and `setTier({ creator, tier, priceCents, status?, key })` sets one (`caps.tier` = `billing.tier.manage`;
1232 `listTiers` reads with the intent capability when `caps.tier` is not given). Without a tier nothing changes.
1233
1234## 0.50.1 (2026-10-17)
1235
1236- `openvibe-sdk/db`: a nested `db.tx` (a savepoint) marks its scope done when it ends, like the outermost one. Async
1237 work started inside a savepoint and still running afterwards (a timer, a fire-and-forget call) no longer keeps
1238 using the transaction's connection after it goes back to the pool.
1239- Outbox: the relay's timers run in the async context the outbox was created in. A `kick()` from inside a
1240 transaction that stays open (an inbox handler that goes on fanning out) used to make the relay's ticks run inside
1241 that transaction, on its connection. OpenVibe.Network logged "Client was closed and is not queryable" for about a
1242 minute after each go-live notice.
1243
1244## 0.50.0 (2026-10-16)
1245
1246- `openvibe-sdk/play`: **cloud saves** — `play.saves.put(title, player, slot, { data, version })`, `get` (null when the
1247 slot is empty), `list` and `remove`, for OpenVibe.Play's new save slots (8 per player per title, 64 KB of JSON each,
1248 versioned: a stale `version` is `409 play.save_conflict`). Same token and capabilities as scores and achievements.
1249
1250## 0.49.1 (2026-10-16)
1251
1252- Outbox: Events' `409 events.loop_detected` is a permanent refusal: the row is marked rejected instead of being
1253 retried with backoff forever (Search retried 18 such events up to 35 times each after a Reviews loop was fixed).
1254 Every other 409 is still retried.
1255
1256## 0.49.0 (2026-10-15)
1257
1258- `openvibe-sdk/download`: `createDownloadClient({ key })` for OpenVibe.Download (the owner's "file storage and backup"):
1259 `files.list/all/get/remove`, `folders.list/create/ensurePath`, `upload({ path | data, name, folderId })` in the
1260 drive's parts, `usage()`, and `sync(dir, { to, delete, dryRun, exclude, onEvent })`, which keeps a local folder backed
1261 up (only what is new or changed: size, and sha256 when the drive has one; folders mirrored; `delete` removes drive
1262 copies whose local file is gone). Authenticates with a sync key from openvibe.download/sync, or a person's token;
1263 waits out 429s; stops on the drive's daily cap or quota with the reason.
1264- The `openvibe-download` command (`bin`): `sync <folder> --to <drive/path> [--delete] [--dry-run] [--exclude NAME]`,
1265 `ls [path]`, `usage`, with `OPENVIBE_DOWNLOAD_KEY`.
1266
1267## 0.48.0 (2026-10-15)
1268
1269- `openvibe-sdk/play`: `createPlayClient(client)` for OpenVibe.Play's game services (the owner's "game services in the
1270 SDK"): `titles.define/list/get`, `scores.post`, `achievements.unlock`, `players.adopt/achievements`, `boards.get`.
1271 Writes use the game's own principal (audience `openvibe.play`, `play.title.manage` / `play.result.write` /
1272 `play.player.read`); titles and leaderboards are public reads without a token; a player is checked (usr_… or
1273 guest:<opaque>) before the request leaves.
1274
1275## 0.47.0 (2026-10-15)
1276
1277- `openvibe-sdk/quest`: `createQuestClient(client)` → `appQuests.define(slug, quest)`, `appQuests.list()`,
1278 `appQuests.retire(slug)`, and `eventType(projectId, name)` / `projectKey(projectId)` for the `app.<project_key>.<name>`
1279 events an app publishes. App quests (contracts 0.193.0, `quest.app_quest.manage`): a developer app defines quests on
1280 its own events on OpenVibe.Quest; the reward is a badge named for the app, never OpenCoins.
1281
1282## 0.46.0 (2026-10-13)
1283
1284- `startTelemetryRetention({ db, table?, olderThanDays = 30, everyMs = 24 h, delayMs = 60 s, log? })` → `{ run(), stop() }`:
1285 the scheduled form of `pruneTelemetry()`. It first prunes `delayMs` after the call, then every `everyMs`, on unref'd
1286 timers. A service that waited a full day for the first prune never pruned when it was redeployed more often than that
1287 (found in review of OpenVibe.Blog#42). A failing prune is logged and retried at the next tick; runs never overlap;
1288 `stop()` cancels the timers and waits for a run in flight.
1289
1290## 0.45.0 (2026-10-13)
1291
1292The telemetry sink every service stores into (plan T1, step 10). Step 5 left `createTelemetry({ sink })` and
1293`createHttpTelemetry({ sink })` without a sink for any service but Network; `openvibe-sdk/telemetry` now ships one,
1294a per-service PostgreSQL table (the decision and its reason are in `docs/service.md`, "Where the samples go"):
1295
1296- `telemetrySchema(table = 'telemetry_samples')`: the DDL for a service migration, `platform.telemetry-sample@1`'s
1297 21 columns in schema order (no `instance`), `extra` jsonb, indexes on `(at)` and `(service, operation, at)`.
1298- `createPgTelemetrySink({ db, table?, log? })`: the sink. One multi-row parameterized INSERT per flush; a malformed
1299 sample (no service, operation or parseable `at`, or a field of the wrong type) is dropped with one warning and the
1300 rest stored; a rejected `db.query` propagates, so the collector keeps the batch for its next flush.
1301- `pruneTelemetry({ db, table?, olderThanDays = 30 })`: the retention delete.
1302- A table name that does not match `/^[a-z_][a-z0-9_]{0,62}$/` throws, so no SQL goes in through it.
1303- **Tests:** `test/telemetry-sink.test.js` on PGlite.
1304
1305## 0.44.0 (2026-10-11)
1306
1307The SQLite kits are gone (plan T1, the deletion ledger): every OpenVibe service has run on PostgreSQL since
13082026-10-08, and nothing in the estate used them. **Breaking** for a caller of what was removed:
1309
1310- `createOutbox` / `createInbox` (`openvibe-sdk/events`, better-sqlite3): use `createPgOutbox` / `createPgInbox` on an
1311 `openvibe-sdk/db` handle (`createDb({ pglite: true })` for a small embedded app), with `outboxSchema()` /
1312 `inboxSchema()` in its migration.
1313- `createRevocationStore` (`openvibe-sdk/auth`, better-sqlite3 or memory): use `createPgRevocationStore`.
1314- The stale `importSqlite` / `runSqliteMigration` type declarations (their code was already gone).
1315- `better-sqlite3` is no longer a devDependency or an optional peer dependency, so it leaves every service's lockfile
1316 with this pin.
1317- **Tests:** the SQLite outbox suite's two cases the PostgreSQL suite lacked move there (a token-endpoint refusal is
1318 retried, never rejected; a flush during a pass also publishes what was committed after it claimed). The delivery
1319 worker test's inbox runs on PGlite.
1320
1321## 0.43.0 (2026-10-10)
1322
1323No sequence number from Events (Contracts 0.136.x, ADR-042 decision 7, its last step). **Breaking** only for code that
1324read `seq`; nothing in the estate does.
1325
1326- **`publish()`** answers `{ event_id, cursor, duplicate }` (a repeat whose stored copy retention removed:
1327 `{ event_id, duplicate: true, pruned: true }`).
1328- **Reads:** `pull()`/`iterate()` items and `get()` are `{ cursor, event }`.
1329- **Realtime:** callbacks get `{ cursor }`.
1330- **`parseDelivery()`:** `{ event, subscriptionId, attempt }`. Dedupe on `event.event_id`.
1331- **Outbox kits:** no longer record a sequence number (a table's old `seq` column stays empty).
1332- **Mock Events:** publish answers carry the cursor; pages, single reads, SSE data and deliveries carry no `seq`
1333 (no `X-OpenVibe-Seq`). The mock's own `platform.publishEvent()` still returns the stored `seq`, for tests that
1334 build a cursor.
1335
1336## 0.42.0 (2026-10-10)
1337
1338An Events position is an opaque cursor and nothing else (ADR-042 decision 7, plan T7). **Breaking** for a caller that
1339still passes a number:
1340
1341- **`pull()` / `iterate()`** take `after` (a page's `next_cursor`, or `latest_cursor` for the head) and nothing
1342 numeric. `afterSeq` throws a `TypeError` instead of silently reading from the start. Without `after`, a page starts
1343 at the oldest retained event, and no `after_seq` is ever sent. `iterate()` ends on the page whose `next_cursor` is
1344 `latest_cursor`, or on one that does not move the position. Pages carry `next_cursor` and `latest_cursor` only
1345 (types: `next_after_seq` and `latest_seq` are gone).
1346- **Checkpoints** store the opaque cursor. `setCheckpoint(topic, cursor, { carrier })` rejects anything but a cursor
1347 string, and `getCheckpoint()` answers `{ consumer, topic, cursor, carrier, updated_at }` with `cursor` as stored,
1348 or null when none is.
1349- **Realtime** resumes from the cursor exactly as Events sent it (`lastEventId` is a string; a bare number is no
1350 longer read as a position), and it dedupes by `event_id` (the last 512), never by position. The callback's
1351 `{ cursor }` is what to save; `seq` is informational and null when absent.
1352- **`parseDelivery()`** answers `seq: null` when a delivery carries none (it was `NaN`). Dedupe deliveries by
1353 `event.event_id`.
1354- **Mock Events** (`openvibe-sdk/testing`) behaves like Events after the change: `after_seq` is a 400, pages have no
1355 numeric fields, checkpoints hold cursors, the realtime `id:` is the cursor and a bare-number `Last-Event-ID` is
1356 ignored.
1357
1358## 0.41.0 (2026-10-10)
1359
1360- **`createResourceIndex().list({ owner })`.** The index passes `owner` (a `usr_…`/`agt_…` subject id) to every
1361 authority on every page, beside `project` and `kind`. An authority answers only what that subject owns. A person's
1362 own resources (robots, rooms, streams, watches, Actor tasks: none of them in a project) can then be listed without
1363 reading every authority's whole index. A console keeps its own filter on the merged page, so an authority that has
1364 not added `owner` yet costs a longer walk but never shows another person's row. `docs/service.md` says what an
1365 authority answers.
1366
1367## 0.40.1 (2026-10-10)
1368
1369- **`verifyUserToken` refuses node principals.** A node token (`actor_type: link`, `sub link:lnk_…`: a machine paired
1370 for a service, such as a robot's computer for OpenVibe.Bot) is signed with Network's key like every principal token.
1371 It was refused for service, app and module principals only, so a service that checks user tokens without an
1372 audience would have taken a node token for a session. It is `token.not_user` now, by its `actor_type` or its `sub`
1373 alone, unless the caller passes `allowServiceTokens`.
1374
1375## 0.40.0 (2026-10-10)
1376
1377- **New: `createNetworkKeys()`** (`openvibe-sdk/auth`, server). The keys a service verifies Network tokens with, as one
1378 object: a pinned key (`publicKey`, e.g. `OV_NETWORK_PUBLIC_KEY`) used as is, or Network's JWKS (`network` or
1379 `jwksUrl`) through the process-wide JWKS client, so a token naming an unknown kid is verified after one refetch
1380 (a rotation) and the last good keys are served through a Network outage. `start()` fetches at once and retries every
1381 30 s until the first load (Network may still be booting), then refreshes every 15 minutes; `stop()` clears both.
1382 `keys.verifyOptions` spreads into `verifyUserToken`, `verifyAppToken` and `verifyServiceToken`; `keys.loaded()` is the
1383 readiness check; `keys.keysFor(kid)` answers the keys for a token type a service checks itself. Services each had
1384 their own key store (one PEM, no kid, a 6 h refresh); this replaces them.
1385- `verifyUserToken` and `verifyAppToken` accept a JWKS client as `jwks` (a URL, a document or a PEM as before), as
1386 `verifyServiceToken` already did.
1387
1388## 0.39.0 (2026-10-10)
1389
1390- **New: `openvibe-sdk/notifications`** (server). `createNotificationsClient(client).push({ subjectId | userId, type, title,
1391 message, url, category, priority, icon, rich_content })` sends a person one notification through OpenVibe.Network
1392 (`POST /internal/notifications/push`, network.notification-push-request@1; capabilities `network.notifications.push`
1393 and, for a subject, `identity.subject.resolve`). It answers `{ sent: true, skipped }` (skipped: the person switched the
1394 category off) or `{ sent: false, reason: 'unknown_subject' }` for a subject Network does not know or a deleted account.
1395 The push is attempted once, because Network keeps no idempotency record for it: callers retry from their own queue.
1396 Search and Watch each had (or were about to write) their own copy.
1397
1398## 0.38.1 (2026-10-10)
1399
1400- **`migrate`: a fresh database applies its contract migrations at once.** When a run starts with no row in
1401 `ov_migrations` (a new install, a test database, a restore drill into an empty schema), no previous release can be
1402 running against it, so the N-1 window has nothing to protect. Before, a fresh database stopped at its first contract
1403 for `windowDays`, and because a held contract holds everything after it, every migration past that point went
1404 untested: OpenVibe.Chat's test databases had stopped at 0004 since its 0005 contract. An existing database is
1405 unchanged: a contract waits for its expand's window, also when both arrive in the same deploy, and holds what follows.
1406- `createTestDb` snapshots are built on a fresh database, so they carry the contracts too.
1407
1408## 0.38.0 (2026-10-10)
1409
1410- **Removed: the one-time SQLite → PostgreSQL migration tooling** (plan T1, "Delete the migration era"). Every
1411 OpenVibe service runs on PostgreSQL and none calls them: `importSqlite` and `runSqliteMigration` (`openvibe-sdk/db`),
1412 `src/db/sqlite-import.js`, `src/db/sqlite-cli.js`, the `tools/asyncify/` conversion scripts and
1413 `docs/migrating-to-postgresql.md`. A consumer that still imported either function must drop it; nothing else changes.
1414- Kept: the better-sqlite3-shaped statements on PostgreSQL (`db.prepare`), and the SQLite outbox and inbox
1415 (`createOutbox`/`createInbox`) for a small app that keeps them in SQLite (OpenVibe.Examples' webhook consumer does).
1416
1417## 0.37.2 (2026-10-10)
1418
1419- `openvibe-sdk/realtime`: each event's callback gets its opaque `cursor` (the SSE id Events sent) beside `seq`:
1420 `subscribe(topics, (event, { seq, cursor }) => …)`. Save the cursor after handling the event and pass it back as
1421 `lastEventId` to resume, so a subscriber never needs the numeric seq (plan T7). `cursor` is null when Events sent no
1422 id. Additive.
1423
1424## 0.37.1 (2026-10-10)
1425
1426- `openvibe-sdk/testing`'s mock Events answers a pull with `latest_cursor` (the head as an opaque cursor), as
1427 OpenVibe.Events does since Events#45. A consumer that starts at "now" stores it and reads on with `after=`,
1428 never needing the numeric `latest_seq`. `events.pull()` documents it. Additive.
1429
1430## 0.37.0 (2026-10-08)
1431
1432- **`openvibe-sdk/account-data`: `startSubscriptions()`**, the boot-time half every adopter had copied. It creates any
1433 missing OpenVibe.Events subscription for the two account topics (or `topics`) to the service's loopback endpoint with
1434 its own token (audience openvibe.events, scope events.subscription.manage), retried with backoff in the background,
1435 and leaves an existing one alone, so a restart creates nothing. It returns null when the Events URL, the delivery
1436 secret or the client secret is unset; `stop()` ends the retries and `done` says whether every topic was subscribed.
1437
1438## 0.36.0 (2026-10-08)
1439
1440- **`openvibe-sdk/account-data` (new, node): a service's share of account export and deletion (ADR-033).** Six services
1441 implemented it by hand; every newer one (Actor, Food, Help, Work, Quest, Rent, Watch…) had none. A service now
1442 declares the tables that hold a person's rows (`{ table, subject, value?, file?, columns?, erase? }`, with `erase`
1443 `'delete'`, `{ anonymize: {…} }` or `{ keep: '<why>' }`) and gets `apply(event, { send })` for
1444 network.account.export_requested / network.account.deleted, a signed `consumer()` handler for POST /internal/events,
1445 `exportPart()`, `erase()` (one transaction, merged-account aliases included), `ensureSchema()` /
1446 `ACCOUNT_DATA_SCHEMA` (account_data_events: idempotent per export/deletion id, so a redelivery never erases twice and a
1447 failed confirmation is sent again) and `createNetworkSender()` (the service's own client-credentials token for
1448 Network's internal routes). Identifiers in the table map are checked when it is made; values are always parameters.
1449
1450## 0.35.2 (2026-10-08)
1451
1452- **The mock developer platform grants `services.release.manage|read` (`openvibe-sdk/testing`, patch):** openvibe-contracts
1453 0.113.0 moved the developer console from OpenVibe.Codes to OpenVibe.Services and retired `codes.release.*`; the mock's
1454 app catalog follows, so a developer-path run against it requests the names Network grants. Pins openvibe-contracts
1455 v0.113.0.
1456
1457## 0.35.1 (2026-10-08)
1458
1459- **A PGlite test database no longer keeps its process alive (`openvibe-sdk/testing`, patch):** PGlite emulates
1460 PostgreSQL's SIGALRM timers with `setTimeout` (its wasm runtime's `__setitimer_js`), and one loaded from a snapshot
1461 (0.29.0) keeps rescheduling a 10 s startup-progress alarm, so a test file that ended without closing its database
1462 never exited and a per-file runner timed it out (OpenVibe.Network's whole suite, OpenVibe.Community's unclosed
1463 tests). `createTestDb` on PGlite now unrefs the timers `__setitimer_js` sets (told apart by the caller's frame, once
1464 per process); queries run synchronously in the wasm and never wait on one. Every other timer, a test's own
1465 included, stays ref'd. A service-side workaround that unrefs every PGlite timer (OpenVibe.Media's preload) can go.
1466- **`createTestDb` on the containers unrefs its lease connection only once the lease is held (patch):** 0.31.1
1467 unref'd the socket before `pg_advisory_lock` answered, so a process with nothing else open (no PGlite alarm since
1468 the fix above) exited mid-setup with code 0, its remaining tests never run.
1469
1470## 0.35.0 (2026-10-07)
1471
1472- **Events public origin moves to `https://openvibe.events` (plan T7):** realtime clients and the mock
1473 platform now use the new origin by default; SDK examples and types name it as well. Browser pages using
1474 the SDK's realtime client must allow `connect-src https://openvibe.events` in their Content Security Policy.
1475 Events serves both origins during migration; the old origin will answer 308 after clients have moved.
1476
1477
1478
1479
1480
1481## 0.34.0 (2026-10-05)
1482
1483- **`openvibe-sdk/resources` (new): the resource index and the control plane (ADR-048, plan T13 step 2).**
1484 `parseResourceName`/`resourceName`/`resourceNameOf` reuse the pinned openvibe-contracts helpers
1485 (`contracts.resources`, required lazily) for the one resource name, OVRN
1486 `ovrn:<service>:<project_id>:<type>/<id>`. `createResourceIndex({ authorities, token, fetch, pageLimit,
1487 concurrency, timeoutMs, maxPages, maxMs })` fans out `GET
1488 {authority}/api/v1/resources?project=&kind=&cursor=&limit=`, follows each authority opaque
1489 `next_cursor` and merges the `common.resource-list-result@1` pages in authority order; an authority
1490 that fails (network, timeout, non-2xx, malformed page) never fails the call — its pages read so far are
1491 kept and it is reported in `stale`, so a read model can be rebuilt. A cursor chain that would not end
1492 is bounded — a repeated cursor (a cycle or an echoed cursor) or an empty `next_cursor` ends the walk,
1493 `maxPages` caps the pages read and an optional `maxMs` deadline caps the time — and the authority is
1494 reported `stale` (`sdk.bad_response`/`sdk.timeout`) rather than hanging `list()`.
1495 `createResourceClient({ origin, token })` POSTs a `common.resource-control-request@1` to
1496 `/api/v1/resources/control` and returns the authority `common.resource-control-result@1`
1497 (`done`/`pending`/`refused`/`failed`, a refused one carrying its problem or `confirmation_required`);
1498 a refused/failed result is returned even when it arrives with a non-retryable non-2xx (403/409/422), so
1499 a confirmation gate is not thrown away. An idempotency key is generated when the caller omits it and a
1500 transient failure (network, timeout, 408/425/429/5xx) is retried with the same key and body, so an
1501 action is never applied twice; an error thrown after the retries carries that `idempotencyKey`, so the
1502 caller's own retry reuses it instead of duplicating a sensitive action.
1503 `RESOURCE_KINDS` lists the kinds whose three-letter id prefix ADR-048 has chosen
1504 (`media.object`/`med`, `watch.watch`/`wch`); the proposed (`act`, `run`, `zon`) and unchosen
1505 (`codes.repo`, `events.queue`, `events.subscription`) kinds are deliberately absent until step 8.
1506
1507## 0.33.0 (2026-10-05)
1508
1509- **`openvibe-sdk/events` surfaces the opaque `after`/`next_cursor` pull cursors (ADR-042 decision 7):** `pull()`
1510 now takes `after` — a page's opaque `next_cursor` — and sends it as the `after` query parameter; `afterSeq` (the
1511 numeric `after_seq`) stays for one release and is sent when no cursor is given. `StoredEvent` carries `cursor`
1512 and `EventsPage` carries `next_cursor`. `iterate()` starts from `after` or `afterSeq` and advances on
1513 `page.next_cursor` (falling back to `next_after_seq` while the compatibility field remains) after `onPage`, so a
1514 page that matched no events still advances the saved cursor; resuming from a saved `next_cursor` replays
1515 nothing. The mock platform (`openvibe-sdk/testing`) models the same page shape and accepts `after`, and
1516 `README.md`'s pagination note names the cursor. New assertions in `test/events.test.js` and
1517 `test/app-events.test.js` cover the round-trip, the empty page, and the `after_seq` fallback.
1518- `iterate()` keeps the mode the caller asked for (a string `afterSeq` such as `'5'` is still the numeric
1519 position) and finds the head without the numeric compatibility fields: once a server sends only cursors,
1520 the page whose `next_cursor` does not move ends the walk.
1521
1522## 0.32.0 (2026-10-05)
1523
1524- **`openvibe-sdk/usage` gains `createUsageReporter` (plan T1 step 7):** the shared reporter Tools, AI, Events and
1525 Bot each rewrote. `createUsageReporter({ db, service, source, table = 'usage_outbox', billingUrl, tokenClient,
1526 audience, fetchImpl, timeoutMs, intervalMs, batchSize, now, log })` owns one service's readings: `key(...parts)`
1527 is `usageKey(service, …)`, `sample(fields)` fills in the reporter's `service` and `source` and defaults `id` to
1528 the idempotency key, and `record(t, reading)` validates the reading against `platform.usage-sample@1`
1529 (via `validateUsageSample`, so a missing openvibe-contracts never blocks the queue) and inserts it
1530 idempotency-keyed in the outbox table inside the caller's transaction (`db.tx`'s handle; a replayed key changes
1531 nothing). The relay is `createPgOutbox` pointed at Billing's `billing.usage.record` (`POST /api/v1/usage`, one
1532 reading per request): `start`/`stop`/`kick`/`flush`/`prune` plus `pending`/`rejected`, and it uses the given
1533 token client (`createServiceTokenClient`, audience `openvibe.billing`). A reading is never dropped — a relay
1534 that cannot reach Billing (down, no grant yet, a missing route, a timeout or a 401/403/404/408/425/429/5xx)
1535 retries with backoff across restarts — and only Billing refusing the reading itself (any other 4xx: 400, 402,
1536 409, 410, 413, 415, 422) marks a row rejected: kept with its error and never sent again, so nothing is billed
1537 twice; a 401 drops the cached token first. The reporter bills whatever reading it is given, so callers must not
1538 record first-party or sandbox traffic: `requireProject: true` makes `record()` refuse a reading without a
1539 `prj_…` project. Without `billingUrl` or `tokenClient` readings still queue and
1540 only the relay is off. Re-exported from the top-level entry and declared in `types/usage.d.ts` (the `./usage`
1541 subpath already existed); `outboxSchema(table)` stays the migration DDL. New unit test for key derivation,
1542 replay refusal and the retry/reject relay (the full status matrix, 401 invalidation and the timer stop).
1543
1544## 0.31.1 (2026-10-05)
1545
1546- **`createTestDb` in pg mode no longer keeps a test process alive (`openvibe-sdk/testing`, patch):** the lease
1547 `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
1548 without `process.exit()` never exited and a per-file runner timed it out (Media's test:pg hung at 300 s per file).
1549 The lease socket is now unref'd right after it connects; the lock still holds for as long as the process lives, and
1550 `close()` still releases it.
1551
1552## 0.31.0 (2026-10-05)
1553
1554- **`openvibe-sdk/service` carries the HTTP telemetry middleware and autoscaling signals (plan T1):** lifted from
1555 OpenVibe.Network's `server/telemetry.js` and `server/observability.js`, which were the only implementation.
1556 `createHttpTelemetry({ service, sink, intervalMs = 15000, routeLabel?, skipExact?, skipPrefixes?, skip?, signals? })`
1557 returns a collector with `middleware()`, `record`/`gauge`/`count`, `requestStarted`, `requestFinished`,
1558 `observeRequest`, `flush` and `stop`. A request is not a row: requests aggregate per `route|method|status_class`
1559 per flush into `count`/`sum`/`max`/`p95`, and one `http.request` `platform.telemetry-sample@1` sample per key is
1560 emitted per flush (its mean in `latency_ms`, the counts in `extra`), plus the active-requests, rolling-p95 and
1561 event-loop-lag gauges once per flush — never per request, so the SDK buffer cannot overflow. A sample carries the
1562 route template, method and status class, never a raw URL or token; health, readiness, metrics and static assets are
1563 skipped (`telemetrySkipped`). The singleton `telemetry.init(...)` / `telemetryMiddleware` / `telemetry.stop()`
1564 wires it process-wide: `init` starts the event-loop monitor and the unref'd flush timer, `stop` stops both and
1565 flushes once (a `gracefulStop` stop step), and requiring the kit starts nothing. `registerSignals({start, stop,
1566 lag})` injects a service's own monitor. No change to the other modules. A request that matched no route is
1567 labelled `'unmatched'` (never its raw path), `skipPrefixes` defaults to `['/shared']` (Network passes
1568 `['/shared', '/api/chrome']`), the per-flush aggregation keys and latency samples are capped (overflow folds into
1569 the `'other'` route label; p95 uses reservoir sampling while count/sum/max stay exact), and a second `init`
1570 stops (flushes) the collector it replaces.
1571
1572## 0.30.0 (2026-10-04)
1573
1574- **`openvibe-sdk/bot` (new, both; plan B8):** the OpenVibe.Bot client. `createBotClient(client, {actingSubject?})`
1575 wraps `robots` — `list`/`get`/`create`/`update`/`delete`/`pairingCode`, `operators.list`/`add`/`remove`,
1576 `devices` (listing), `streaming.get`/`set`, `audit`/`iterateAudit`, `estop`/`clearEstop` and `command`
1577 (`bot.command@1`, answered by Bot's own idempotency cache) — plus `devices.rotate`/`revoke`, the public
1578 `profiles`/`kits` reads, and `pair` (the agent redeems a one-time code; never retried). A service acts for a
1579 person with `X-OV-Subject` (`bot.robot.read`/`manage`/`control`, `bot.device.connect`); a browser calls as the
1580 signed-in person. Browser-safe, with its ESM entry, types and the browser bundle, and a test over the
1581 Contracts bot fixtures. The operator WebSocket and the internal Run→Bot job dispatch are not wrapped.
1582
1583## 0.29.0 (2026-10-04)
1584
1585- **`createTestDb` reuses a migrated snapshot (`openvibe-sdk/testing`, minor):** on PGlite, the first test process
1586 boots, migrates, runs the new `seed: async (db) => {}` hook and saves the data directory (`dumpDataDir`) under
1587 `os.tmpdir()/openvibe-test-snapshots` (`OV_TEST_SNAPSHOT_DIR`); later processes load it (`loadDataDir`) instead of
1588 migrating, refresh `ov_migrations.applied_at` (so an ADR-028 contract migration stays held as on a fresh migrate) and
1589 run `migrate` only if the snapshot is behind. The key is the migrations' file names and contents, the
1590 SDK and `@electric-sql/pglite` versions and `seedKey` (default: the seed function's source; a `seedKey` without a
1591 `seed` is not the same snapshot as one with both). Parallel processes build
1592 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
1593 deleted and rebuilt. The result adds `snapshot` (`'hit'`, `'built'` or `'off'`) and `setupMs`. Each database is
1594 still the process's own: rows a test writes are never in the snapshot. **Opt out** with `OV_TEST_SNAPSHOT=0`.
1595 On `store: 'pg'`, `seed` runs on the run's schema after the migrations with the schema owner's rights (as on PGlite)
1596 and `setupMs` is reported; roles, privileges
1597 and the lease are unchanged.
1598
1599## 0.28.0 (2026-10-04)
1600
1601- **`openvibe-sdk/commerce` (new, server; plan T5 step 13):** the Billing client Tips and VIP each carried a copy of.
1602 `createCommerceClient(config, { caps, fetchImpl, tokenClients, timeoutMs })` with `createIntent` (purchase or
1603 subscription), `createTransfer`, `refundTransfer`, `subscribeWithCredit`, `cancelSubscription`, `getSubscription`,
1604 `listSubscriptions`, `entitlement`, `rates` and `baseUrl`. One call shape: an `Idempotency-Key` is sent verbatim, a
1605 401 invalidates the token and retries exactly once with the same key, and a problem+json answer becomes a
1606 `CommerceError` (`status`, `code`, `body`; `retryable` for no status, 5xx, 429, 401 and `billing.frozen`).
1607 `intentKey(prefix, parts)` derives a key and `receipt(kind, fields)` builds an outbox record (never posted).
1608 `openvibe-contracts` is required lazily, only when a token client is created, so the tarball still loads with no
1609 dependencies. No change to the other modules.
1610
1611## 0.27.0 (2026-10-04)
1612
1613- **`openvibe-sdk/cost` (new, server; plan T1):** `rateCard()` / `validateRateCard()` for `platform.rate-card@1`,
1614 `costSnapshot()` / `validateCostSnapshot()` for `platform.cost-snapshot@1`, and `estimate(card, quantity, unit)`
1615 for the charge a rate card gives a quantity.
1616- **`openvibe-sdk/runtime` (new, server; plan T1):** `runtimeOffer()` / `validateRuntimeOffer()` for
1617 `platform.runtime-offer@1`, and `runtimeClass()` / `validateRuntimeClass()` / `RUNTIME_CLASSES` for
1618 `platform.runtime-class@1`.
1619- **`openvibe-sdk/storage` (new, server; plan T1):** `storageOffer()` / `validateStorageOffer()` for
1620 `platform.storage-offer@1`.
1621- **`openvibe-sdk/delivery` (new, server; plan T1):** `deliveryOffer()` / `validateDeliveryOffer()` for
1622 `platform.delivery-offer@1`.
1623- **`openvibe-sdk/node` (new, server; plan T1):** `nodeCapabilities()` / `validateNodeCapabilities()` for
1624 `platform.node-capabilities@1`, and `nodeOffers(descriptor)`, which derives a node's resource and runtime offers.
1625- **`openvibe-sdk/placement` exports `TRUST_CLASSES`** (the five T1 trust classes) and `TRUST_ORDER` (the planner's
1626 default preference, unchanged), with their type declarations.
1627- **`openvibe-sdk/usage` (new, server; plan T1):** `platform.usage-sample@1` readings in one place.
1628 `usageSample(fields)` builds the record — `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit` and
1629 every optional dimension pass through, `at` defaults to now (ISO) and `source` to `openvibe-sdk/usage`, null fields
1630 are stripped and no money field is invented (`cost_estimate`, `free_allowance_used`, `vibes_charged` are the
1631 caller's; rating is Billing's). `usageKey(service, ...parts)` joins an idempotency key with `:` (the service alone
1632 when there are no parts; a non-empty service is enforced). `validateUsageSample(record)` → `{ ok, errors }`
1633 (`openvibe-contracts` is required lazily on first use; a missing package never claims validity: `ok: false`,
1634 `errors: []`). `openvibe-sdk/govern` now builds its `onUsage` record with `usageSample()`: the record is unchanged.
1635- **`openvibe-sdk/telemetry` (new, server; plan T1):** `platform.telemetry-sample@1`, the universal operation
1636 observation. `telemetrySample(fields)` and `validateTelemetrySample(record)` as in usage;
1637 `createTelemetry({ service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000 })` →
1638 `{ record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop() }`: a collector
1639 that buffers samples and flushes them to `await sink(samples)` every `intervalMs` (an unref'd timer) and on
1640 `stop()` (clears the timer, flushes once — a `gracefulStop` stop step). `record` builds
1641 `{ service, instance, operation, at, latency_ms, ...labels }`; a value that is not a number goes to `extra[name]`
1642 as a scalar. A sink that rejects is logged once and its samples kept for the next flush; past `maxBuffered` the
1643 oldest sample is dropped, with a warning at most once a minute. The SDK holds no network code: the sink is the service's.
1644- **`openvibe-sdk/placement`:** `TRUST_ORDER` gains `user-owned` (between `first-party` and `partner`; the T1 trust
1645 classes): a `user-owned` offer is eligible under `trust: ['user-owned']` and for workloads that name no trust list.
1646
1647- The devDependency `openvibe-contracts` moves to v0.86.0 (it adds a `node` principal to
1648 `identity.service-token-claims`); the copied types and the lockfile integrity hash follow.
1649- **`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.
1650
1651## 0.26.0 (2026-10-01)
1652
1653- `openvibe-sdk/service` (new, server; plan T1): the service kit. `gracefulStop({ name, server, stop, close, drainMs = 4000,
1654 deadlineMs = 5000, deadlineExitCode = 1, signals = true, exit, log, beforeDrain, handles })` → `{ stop(signal?),
1655 stopping() }` and `within(ms, promise)`: OpenVibe.Network's and OpenVibe.Community's `server/graceful.js` (stop steps,
1656 then `server.close` with `Connection: close` on requests in flight, idle connections closed every 50 ms and event streams
1657 destroyed, then close steps, exit 0; one stop per process), plus the exit code of a blown deadline (the 5 s family and
1658 Media exit 0), `beforeDrain` and `handles` (closed last; a failure exits 1). Its deadline timer is not `unref()`'d, so a
1659 step stuck on a promise ends in the deadline's exit code rather than a quiet exit 0. The error layer: `createServiceError(name)`,
1660 `ServiceError`, `asServiceError(err, { publishing, map })`, `run(fn, status)`, `wrap(fn)`, `sendError(res, req, err, log)`,
1661 `jsonBody({ limit = '512kb' })` (malformed: 400 `request.invalid_json`; too large: 413 `request.too_large`),
1662 `privateNoStore(res)` and the opt-in `jsonErrors()`, with Reviews/Wiki's defaults and `extra` spread (or `extra: 'details'`
1663 for Tips/VIP). `createReadiness`, `skip`, `safeReason`, `createRegistry`, `instrument`, `metricsHandler`, `isLoopbackDirect`,
1664 `releaseInfo`, `createRelease` (openvibe-shared) and `problem`, `sendProblem` (openvibe-contracts) are re-exported, required on
1665 first use from the service's own packages: the SDK gains no dependency. Recipe per family: `docs/service.md`.
1666- `scripts/esm.js`: a getter on a CommonJS entry (a lazy re-export) becomes an ESM function that reads it on call, so
1667 importing `openvibe-sdk/service` loads neither package.
1668
1669## 0.25.2 (2026-09-29)
1670
1671- `openvibe-sdk/realtime` follows Events' opaque cursors (ADR-042): it resumes from the last SSE id exactly as Events
1672 sent it (a cursor, or a seq from an older Events) and dedupes on the `seq` in the message data. With cursor ids the
1673 previous release parsed the id as a number, so its resume position and repeat filter stopped advancing.
1674 `subscription.lastEventId` is now that raw id (a string).
1675
1676## 0.25.1 (2026-09-29)
1677
1678- `verifyServiceToken({ jwks })` also takes a JWKS client the service already holds (`jwksClient(url)`, or one a test
1679 injects), so a service that built one for `openvibe-sdk/sso` verifies service tokens with the same keys, chosen by
1680 `kid`, instead of taking the first key itself.
1681
1682## 0.25.0 (2026-09-29)
1683
1684- `openvibe-sdk/sso` (new, server): `createSsoClient({ site, baseUrl, clientId, clientSecret, networkUrl, networkInternalUrl,
1685 secureCookies })` → `router(express)` (`/login`, `/callback`, `/fedcm`, `/logout`, `/me`, `/refresh`), `optionalAuth()`,
1686 `requireAuth()`, `verify()`, `extractToken()`. A product site's sign-in with OpenVibe.Network in one call: state and PKCE
1687 S256 on every sign-in, the code exchanged server-side, `next` limited to this site and the Network, offline verification
1688 through the shared JWKS client. Replaces the copies in Blog, Coupons, Deals, Host, News and Trade (plan T1).
1689- `openvibe-sdk/events` `createServiceOutbox({ db, source, eventsUrl, clientId, clientSecret, eventTypes, validate })`: a
1690 service's events outbox in one call around `createPgOutbox` (`emit` joins the change's transaction, `emitIn(t, …)`,
1691 `moderationAction`, `start`/`stop`/`kick`/`status`); the relay is off, and says so, without the events URL or the
1692 client secret. Replaces the wrapper nine services copied (plan T1).
1693- No floating promises (plan T0): the Valkey queue logs a job whose run rejects outside its handler (a malformed stream
1694 entry was an unhandled rejection), and the mock jobs service fails a job whose background run throws.
1695
1696## 0.24.0 (2026-09-29)
1697
1698- `openvibe-sdk/auth` `verifyServiceToken(token, { jwks | publicKey, issuer, audience, contracts, acceptSandbox, log })`:
1699 a service or app token for a service that receives them. The SDK picks the key (the one the `kid` names, else each,
1700 from the shared JWKS client or a pinned PEM); every rule is the service's own pinned openvibe-contracts
1701 `serviceAuth.verifyServiceToken`, passed in as `contracts`, so the SDK carries no copy of the rules. No key is
1702 `token.unavailable` with the fixed reason 'signing key not loaded yet' (the fetch error goes to `log`). Replaces the
1703 hand-written kid-and-contracts loop in AI, Search, Sources, Games and Codes (plan T1).
1704- `verifyUserToken` refuses a typed token (`typ` or `purpose` set: Network's realtime ticket, a FedCM assertion) as
1705 `token.not_user`: it is never a session, whatever its issuer and audience say. Network's session tokens carry neither.
1706
1707## 0.23.1 (2026-09-29)
1708
1709- 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,
1710 so version, bundle and pack tests failed on its tree).
1711- JWKS: a malformed entry in `keys` (null, a non-object) is skipped instead of marking the whole key set failed; a
1712 legacy document with only `public_key` no longer refetches every 30 s for a token that names a kid.
1713- Types: `log` is declared on the verify options (it is passed to the JWKS client).
1714
1715## 0.23.0 (2026-09-29)
1716
1717- **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.
1718
1719## 0.22.0 (2026-09-28)
1720
1721- **`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).
1722- **`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.
1723
1724## 0.21.3 (2026-09-28)
1725
1726- `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.
1727- `tools/asyncify/sqlfix.py`: the outbox-envelope rewrite no longer nests on a second run.
1728
1729## 0.21.2 (2026-09-28)
1730
1731- `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.
1732- `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.
1733
1734## 0.21.1 (2026-09-28)
1735
1736- `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.
1737
1738## 0.21.0 (2026-09-28)
1739
1740- `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).
1741- `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.
1742
1743## 0.20.4 (2026-09-28)
1744
1745- `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`.
1746
1747## 0.20.3 (2026-09-28)
1748
1749- `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`.
1750- 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).
1751
1752## 0.20.2 (2026-09-28)
1753
1754- **`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`.
1755
1756## 0.20.1 (2026-09-28)
1757
1758- **`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.
1759
1760## 0.20.0 (2026-09-28)
1761
1762- **`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).
1763- **`db.stats().open`**: transactions in progress on this handle.
1764- `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.
1765
1766## 0.19.0 (2026-09-28)
1767
1768- **`openvibe-sdk/db` `runSqliteMigration({ service, sqlite, directUrl, migrations, tables, argv })`** is a service's `scripts/migrate-to-postgres.js` in one call. It:
1769 - applies the migrations as the owner, or on an in-memory PGlite with `--pglite` for a rehearsal;
1770 - imports with `importSqlite` into emptied tables;
1771 - 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;
1772 - verifies counts and checksums, prints the report (`--json` for all of it), and answers 0 only when everything verified.
1773
1774 It was generalised from Tips' script and first used by Wiki. `test/sqlite-cli.test.js`.
1775
1776## 0.18.1 (2026-09-28)
1777
1778- **`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.
1779- **`openvibe-sdk/testing`:**
1780 - `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.
1781 - Setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple concurrently updated").
1782 - `open()` gives a second pooled handle, and `close()` ends the roles' backends and drops them.
1783 - `createTestValkey({ prefix })` gives the containers' Valkey under a prefix of its own.
1784 - Every service's copied test helper is replaced by these.
1785
1786## 0.18.0 (2026-09-28)
1787
1788This release makes the move from better-sqlite3 mostly mechanical: call sites keep their shape and gain `await`.
1789
1790- **`openvibe-sdk/db`: ambient transactions** (on by default; `createDb({ ambient: false })` turns them off).
1791 - 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.
1792 - A `db.tx` inside is a savepoint.
1793 - A promise the transaction did not await runs on the pool once it has ended.
1794 - `db.detached(fn)` leaves the transaction on purpose, and `db.inTransaction()` tells whether code is inside one.
1795 - The handle `fn` receives works as before.
1796- **`db.prepare(text)`**: async statements shaped like better-sqlite3's.
1797 - Methods: `get` (a row or `undefined`), `all`, `run` (`{ changes, rows, lastInsertRowid }`; the id needs `RETURNING`) and `pluck()`.
1798 - Parameters: `?`, or `@name`/`:name` from one object (a repeated name is one parameter).
1799 - The compile to `$n` leaves `'…'` strings, `"…"` identifiers, dollar quotes, comments and `::` casts alone.
1800- Tests: `test/db-ambient.test.js`, on PGlite and through PgBouncer.
1801- `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).
1802- 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.
1803
1804## 0.17.0 (2026-09-28)
1805
1806- **`openvibe-sdk/geo`** (browser and server): `createGeoClient(client, { samples?, timeoutMs? })` finds the closest OpenVibe Link (roadmap WS-X1 task 5).
1807 - `nodes({ role?, region? })` reads Network's public node registry, `GET /api/v1/nodes` (`network.node-list-result@1`), without sending a token.
1808 - `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.
1809 - `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`.
1810- The root export and the browser bundle include `geo`. The root export also includes `openre`.
1811
1812## 0.16.0 (2026-09-28)
1813
1814- **`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.
1815 - `streams`: `list`, `byExternalRef`, `create`, `get` (null when missing), `update`, `delete`, `keys`, `rotateKey`, `destinations` and `addDestination`.
1816 - `destinations`: `update`, `delete`, `test`, `start`, `stop` and `logs`.
1817 - `sessions`: `list`, `get` (null when missing), `playback` (cached 10 s), `end` and `outputs`.
1818 - Also `outputLogs`, `workers` and `manageUrl`.
1819 - Every call uses a service token for `openvibe.openre`. `{ subject }` names the person a call acts for (`X-OV-Subject`).
1820
1821## 0.15.0 (2026-09-28)
1822
1823**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.
1824- **`createPgOutbox(db, { events })`:** takes an openvibe-sdk/db handle.
1825 - `enqueue(t, envelope)` takes the transaction handle, so the event exists if and only if the change commits.
1826 - 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.
1827 - Otherwise it behaves as before: backoff on transient failures, permanent refusals isolated per row, the seq kept, `pending`, `rejected` and `prune`.
1828- **`createPgInbox(db)`:** `once(consumer, eventId, async (t) => …)` runs the handler once, in one transaction with its receipt, and a failed handler leaves no receipt.
1829- **`outboxSchema()` and `inboxSchema()`:** the DDL to put in a service migration, since the runtime role cannot create tables.
1830- **Quieter tests:** the slow-query log is off by default on PGlite, whose first query includes its WASM start.
1831- **Tests:** `test/pg-outbox.test.js` runs on PGlite, and through PgBouncer with three relays sharing one table (60 events, none sent twice).
1832
1833## 0.14.0 (2026-09-28)
1834
1835**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.
1836- **`openvibe-sdk/db`:**
1837 - **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.
1838 - **Transactions:** `tx(fn, { isolation, retries })` retries 40001 and 40P01 with backoff, and nested `t.tx()` calls are savepoints.
1839 - **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.
1840 - **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.
1841- **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`.
1842- **`importSqlite()`:** the one-time move of a service's SQLite file into its PostgreSQL schema.
1843 - 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).
1844 - Identity values are kept and sequences advanced. A source column that would be lost is refused unless dropped on purpose.
1845 - Streamed verification compares counts and content checksums.
1846- **`openvibe-sdk/valkey`:** the shared connection. It is auto-pipelined, confined to `VALKEY_PREFIX`, has a `duplicate()` for blocking consumers, and `ready()`.
1847- **`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.
1848- **`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.
1849- **`openvibe-sdk/pubsub`:** fan-out across processes and hosts, inside the prefix.
1850- **`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.
1851- **Dependencies:** `pg`, `@electric-sql/pglite`, `iovalkey` and `better-sqlite3` are optional peers, loaded only by the module that needs them.
1852- **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.
1853
1854## 0.13.0 (2026-09-28)
1855
1856**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.
1857
1858## 0.12.0 (2026-09-28)
1859
1860**`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.
1861- The actor is the verified principal (`svc:live`, `app:…`), else the signed-in person (`user:<subject>`), else `ip:<address>`; `actor(req)` overrides it, and null skips counting.
1862- A route's limits replace the defaults window by window.
1863- Counters are per process and bounded (`maxActors`, oldest dropped first), and `onLimited` observes refusals.
1864Node only. Additive.
1865
1866## 0.11.0 (2026-09-25)
1867
1868Two 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.
1869
1870## 0.10.0 (2026-09-25)
1871
1872**`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.<key>` parameters. In the browser bundle too. Additive.
1873
1874## 0.9.1 (2026-09-24)
1875
1876The 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.
1877
1878## 0.9.0 (2026-09-24)
1879
1880`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.
1881
1882## 0.8.0 (2026-09-24)
1883
1884`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:
1885
1886- 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}`);
1887- the shared footer, with network and legal links, the "shipped X ago" line and an Updates link;
1888- the theme loader.
1889
1890These 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.
1891
1892`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 `<script>`. `frameConfig` and `scriptUrls` are exported for custom setups. The base must be an https origin (or `http://localhost` in development). Browser-safe; no dependencies.
1893
1894## 0.7.0 (2026-09-24)
1895
1896`openvibe-sdk/vip`: the consumer seam for products that honour OpenVibe.VIP memberships
1897(`createVipClient`, `createVipCache`), published once here instead of the verbatim copies of VIP's
1898`openvibe-vip/client` that Chat, Community and Blog carried. Server-only (`"browser": null`); the
1899same API and fail-closed behaviour: VIP unreachable, a timeout, a refused token or a malformed answer
1900is a denial, a refused token is invalidated and retried once, and the cache drops a member's answers
1901for a creator on `vip.membership.changed` (and Billing's entitlement, cancellation and reversal
1902events) instead of waiting out its TTL. Additive.
1903
1904## 0.6.0 (2026-09-23)
1905
1906The OpenVibe.Tools platform API (ADR-027, openvibe-contracts v0.33.0) as `openvibe-sdk/tools`, and
1907retry and result references in `openvibe-sdk/jobs`. The Tools routes are being built: the registry
1908(`GET /api/v1/tools…`) comes with Tools S3, the run API and the gateway's `/api/v1/jobs` facade with
1909Tools S6. Until they are deployed this client is tested against the contracts and the mock only.
1910
1911- **`createToolsClient(client, { baseUrl?, service = 'tools', audience = 'openvibe.tools', anonymousReads = true })`**
1912 -> `{ list, get, schema, run, jobs }`, on the gateway (the `tools` origin from the platform
1913 descriptor unless `baseUrl` is given). Also in `index.js`, `browser.js`, the ESM entry
1914 (`esm/tools.mjs`) and the browser bundle (`tools` namespace).
1915 - `list({ family, q, execution, api, status })` answers `tools.tool-list@1` (schemas as `$ref`);
1916 `get(id)` the descriptor (`tools.tool@1`, schemas embedded); `schema(id)` `{ $schema, $id,
1917 $defs: { input, output } }`. Both are `null` on 404. Registry reads send no token unless
1918 `anonymousReads: false`.
1919 - `run(id, input = {}, { files, waitMs, idempotencyKey, signal, timeoutMs })` posts
1920 `tools.run-request@1`. An inline tool, or a job that finished within `waitMs`, resolves to
1921 `{ state: 'succeeded', tool, result: { data | text | files }, took_ms, job?, location? }`. A job still
1922 queued or running (202, or a replay) resolves to `{ state, tool, job, location }`. Both carry
1923 `idempotencyKey` and `replayed`, and a non-enumerable `wait(opts)`: a finished run resolves to
1924 itself, and a job follows its events (`jobs.wait`) to the succeeded run.
1925 - A run that finished `failed` or `cancelled` throws **`ToolRunError`**, an `OpenVibeError` (same
1926 `name`, `isOpenVibeError` is true) carrying the tool's problem+json (`code`, `status` of the
1927 problem, `title`, `detail`, `errors`) plus `state`, `tool`, `job` and `run`; `isToolRunError()`.
1928 A request refused before the tool ran (404 `tools.tool.not_found` | `not_runnable`, 422
1929 `tools.input.invalid`, 401/403, 429, 503 `tools.tool.unavailable`) is a plain `OpenVibeError`.
1930 - The `Idempotency-Key` is always sent (generated when omitted), so the core client retries a
1931 429 after its `Retry-After`, and 5xx, with the same key: a job is created once. `timeoutMs` per
1932 attempt is raised to `waitMs`, or to the tool's `limits.timeoutMs` once this client has read its
1933 descriptor, plus 10 s.
1934 - **Files**: uploads in the jobs client's shapes (a Blob/File, or `{ name, data, type? }`) go as
1935 multipart `file` parts; references `{ media_id }` (a Media object you may read) and
1936 `{ job_id, index }` (a result file of your own job) go in the JSON body's `files`, or, beside
1937 uploads, as a multipart `files` part holding their JSON. The tool gets uploads first, then
1938 references. Bad references are a `TypeError` before anything is sent.
1939 - `jobs` is `createJobsClient()` on the same origin and credentials (the gateway facade).
1940- **`openvibe-sdk/jobs`:** `retry(id)` -> `{ job, replayed }` (a failed job as a new one; asking
1941 again returns that same retry), `reference(id, ref)` and `unreference(id, ref)` -> the job (keep a
1942 succeeded result while `<service>:<kind>:<id>` points at it; `expires_at` is null meanwhile).
1943 **Default origin:** without `baseUrl` the jobs client uses the `tools` origin
1944 (https://openvibe.tools), whose gateway fronts every satellite's jobs once Tools S6 ships the
1945 facade. Until then pass the satellite (`baseUrl: 'https://img.openvibe.tools'`); an explicit
1946 `baseUrl` keeps working as before. `Job` types gain `tool`, `retry_of`, `retried_by`,
1947 `references` and `links.retry` / `links.retried_by`.
1948- **Mock platform (`openvibe-sdk/testing`):** `tools: true | { descriptors, handlers, mediaObjects, stepMs }`
1949 serves the registry and run routes on `origins.tools`, with the default tools `dns`, `jsonminify`,
1950 `png`, `port`, `yt` and `protectpdf`. It models caller tiers (probes need `tools.net.probe`, and
1951 job tools need a token because the mock has no browser sessions), the refusal codes, idempotent job
1952 runs, `wait_ms`, and file references. New: `addTool()`, `addMediaObject()`, `state.tools`. **Mock
1953 jobs changed** so that they answer `tools.job@1` exactly: `error` is problem+json (`type`,
1954 `title`, `status`, `code`, `detail`), not `{ code, detail }`; result files carry `sha256`,
1955 `storage: 'local'` and `media: null`. Views also gain `retry_of`, `retried_by`, `references`,
1956 `links.retry` and `expires_at`. Jobs now serve `POST /:id/retry` and `PUT|DELETE /:id/references/:ref`,
1957 and `origins.tools` acts as the gateway facade, so it also finds the satellites' jobs. A test that
1958 compared a failed mock job's `error` with `{ code, detail }` must compare those fields instead. The
1959 mock `fetch` now rejects on an aborted signal, as `fetch` does.
1960- **Contracts:** `openvibe-contracts` v0.33.0 is a devDependency (tarball tag pin, tests only; still no
1961 runtime or peer dependency). `test/tools.test.js` validates every run request and every answer
1962 with it. `types/contracts.d.ts` is re-copied from it: `ModuleNamespace` gains its doc comment, and
1963 it adds `ToolDescriptor`, `ToolList`, `ToolsRunRequest`, `ToolsRun`, `ToolsJob` and
1964 `ToolsJobRequest`. CI no longer clones Contracts v0.28.0.
1965- Types: `types/tools.d.ts` (`ToolsClient`, `ToolRunOutcome`, `ToolRunSucceeded`, `ToolRunPending`,
1966 `ToolRunError`, `ToolFileRef`, `ToolRunFile`, `ToolSchema`, `ToolListQuery`, `ToolRunOptions`),
1967 plus the jobs and testing additions.
1968
1969## 0.5.0 (2026-09-23)
1970
1971Media's object API v2 (`/api/v2/:app/objects`) and its jobs (`/api/v2/:app/jobs`), wrapped as
1972`createObjectsClient()` in `openvibe-sdk/media`. Additive: nothing existing changes.
1973
1974- **`createObjectsClient({ app, baseUrl?, tokenClient | apiKey | client, … })`.** `tokenClient` is a
1975 `createServiceTokenClient()` (a service or developer-app principal with `media.object.upload` and
1976 `media.object.read` for namespace `app`; a developer app uses its project id as `app`). Options:
1977 `actingUserId` (X-OV-User-Id, app key only), `subject` (X-OV-Subject), `multipartThreshold` (64 MiB),
1978 `partSize` (16 MiB), `concurrency` (4), `hashMaxBytes` (256 MiB), `resumeRounds` (3).
1979- **Discovery.** Without `baseUrl`, Media's origin comes from the platform descriptor
1980 (`/.well-known/openvibe`). The descriptor gives an origin only for live services: when Media has
1981 none (not registered, or only a `planned_origin`) the client throws `sdk.service_unavailable` and
1982 never guesses. `objects.baseUrl()` says which origin is used.
1983- **`upload(data, { kind, visibility, mimeType, filename, metadata, contentHash, multipart, onProgress, signal, … })`**
1984 picks single or multipart by size. Single: init, then the bytes go to Media's presigned PUT URL (no
1985 credential on that request), then complete. Multipart (above `multipartThreshold`, with
1986 `multipart: true`, or when Media answers 413 `media.object.too_large` to a single part): parts go up
1987 `concurrency` at a time with `X-Content-SHA256`; after each round the session is read back and the
1988 missing parts are sent again; complete names every part's sha256. The whole object's sha256 is sent
1989 for Media to verify up to `hashMaxBytes`. A part that cannot be sent ends in `sdk.upload_incomplete`
1990 with `err.resume = { objectId, uploadId, missing }`.
1991- **`resume({ objectId, uploadId }, data)`** continues a multipart session, e.g. from another process:
1992 it reads the session with the client's credential, re-sends parts Media holds with a different sha256
1993 and the missing ones, and completes.
1994- **`get(id)`** (null on 404; `med_…` ids and legacy refs), **`signedUrl(id, { ttl })`** (`{ url,
1995 expires_at, public }`: a signed link for private objects), **`delete(id)`** (true, or false on 404),
1996 **`list(q)`** (one page) and **`iterate(q)`** (every object, newest first).
1997- **`jobs.create({ type, objectId, params, idempotencyKey })`**, `get`, `list`, `approve` (a proposal ->
1998 queued), `cancel` and `wait(id)` (polls until succeeded, failed or cancelled). Types today:
1999 `thumbnail.regenerate`, `invariant.scan`, `object.split`, `object.remux`.
2000- Types (`MediaObject`, `MediaJob`, `ObjectsClient`, `ObjectUploadOptions`), the ESM entry and the
2001 browser bundle include it.
2002
2003## 0.4.0 (2026-09-23)
2004
2005Replay protection for OpenVibe.Events webhook deliveries (signature v2). Backward compatible:
2006existing callers of `parseDelivery(raw, headers, secret)` and `verifyDelivery()` keep working, and
2007v1-only deliveries are still accepted until you opt in to `requireV2`.
2008
2009- **Events sends v2.** Besides the unchanged `X-OpenVibe-Signature: sha256=<HMAC of the raw body>`,
2010 every delivery attempt (retries included, each with a fresh time) carries
2011 `X-OpenVibe-Timestamp: <unix seconds>` and
2012 `X-OpenVibe-Signature-V2: t=<ts>,v2=<hex HMAC-SHA256 of "<ts>.<raw body>">`.
2013- New `verifyDeliveryV2(rawBody, headers, secret, { toleranceSec = 300, now = Date.now() })`:
2014 constant-time check of the v2 signature, false when the timestamp is more than `toleranceSec`
2015 from `now` either way or when `X-OpenVibe-Timestamp` disagrees with `t`. Several `v2=` values
2016 are allowed (any one may match); a bad `toleranceSec` throws.
2017- `parseDelivery(rawBody, headers, secret, { requireV2 = false, toleranceSec, now })`: when a v2
2018 header is present it must verify and be fresh, and a bad or stale v2 returns `null` (it never
2019 falls back to v1); without a v2 header, v1 is accepted only while `requireV2` is false. Header
2020 lookup on a plain object is now case-insensitive. **Behaviour change**: a delivery that carries
2021 v2 is now refused when your clock is more than 300 s off Events' (keep NTP on), or when the v2
2022 signature does not verify even if v1 does. Conversely, once v2 verifies, v1 is not consulted:
2023 a test that blanks only `X-OpenVibe-Signature` on an Events delivery and expects a refusal must
2024 drop or break `X-OpenVibe-Signature-V2` too.
2025- New `signDeliveryV2(rawBody, secret, timestamp?)` and `signDeliveryHeaders(rawBody, secret,
2026 { now })` (the three signature headers, for tests that post deliveries to a consumer).
2027 `verifyDelivery` and `signDelivery` (v1) are unchanged.
2028- **Mock Events** `deliverEvents()` / `startDeliveries()` send v1 and v2 headers, signed afresh on
2029 every attempt, as production does.
2030- Types and ESM entry points include the new functions.
2031
2032## 0.3.1 (2026-09-23)
2033
2034The remaining gaps OpenVibe.Examples found, so its examples need no local stand-ins for Events or
2035Media. Contract types re-copied from openvibe-contracts v0.28.0 (adds `codes.app-manifest@1` as
2036`AppManifest`); CI checks them against that tag.
2037
2038- **Events, developer apps.** New `projectKey(projectId)` (`prj_01JAB…` -> `p01jab…`),
2039 `appSource(appId)` (`app_01JAB…` -> `app-01jab…`) and `createAppEvents(client, { projectId,
2040 appId, onBehalfOf? })`: the EventsClient calls with project-relative types and patterns
2041 (`order.shipped` -> `app.<project_key>.order.shipped`, `*` -> `app.<project_key>.*`), the app's
2042 source, and the app (or the `onBehalfOf` person) as actor and default subject; `pull`/`iterate`
2043 take `platformTopics` for public first-party events; subscriptions and checkpoints are scoped the
2044 same way. Types and ESM included.
2045- **Media.** For a sandbox file (`sandbox: true` from Media) the client now sets `public_url: null`
2046 and adds `signed_url` (Media's signed, expiring `url`); it used to put the signed URL in
2047 `public_url`. Media's own fields (`url`, `sandbox`, `url_expires_at`, `app_id`) are passed
2048 through unchanged. **Behaviour change** for code that read `public_url` of sandbox files.
2049- **Mock Events** plays OpenVibe.Events' developer-app rules: app tokens judged on
2050 `events.app.publish | read | subscribe` only (sandbox accepted there); `app.<project_key>.*`
2051 types, `app-<ulid>` source, actor the app or its `on_behalf_of` user; reads, `get`, checkpoints
2052 and subscriptions limited to the own project in the token's env plus public first-party events;
2053 https endpoints that are not loopback, private or local names; events stored with `project_id`
2054 and `env`, deliveries never cross projects or environments; first-party readers and subscribers
2055 see no sandbox events and app events only through `app.*`; realtime streams no app events and
2056 refuses app tokens. Envelopes are checked against the envelope patterns (3+ segment types,
2057 source syntax), topic patterns against Events' syntax, and first-party services cannot publish
2058 `app.*`. Not modelled: per-project quotas, revocation, DNS resolution of endpoints.
2059- **Mock Media** follows Media's tenantAuth: `media.object.upload` uploads and deletes,
2060 `media.object.read` lists and gets, for app keys, service tokens and app tokens alike (service
2061 tokens used to be upload-only). A developer app reaches only `/api/v1/<its project_id>/files`;
2062 its token's env picks the tenant, `prj_…` or `prj_…-sandbox` (created on first use, 1 GB and
2063 100 MB, `mediaQuotaMb`), and Media's tenant-tagged keys are used. Sandbox app tokens are accepted
2064 there without `acceptSandbox`, as in production (**behaviour change**: the mock used to refuse
2065 them). Sandbox files answer with `sandbox: true`, a signed `url` and `url_expires_at`; the new
2066 `GET /f/:key` serves files, sandbox ones only with a valid signature (404 otherwise). Projects no
2067 longer get an API-key tenant in `mediaApps`.
2068- **Mock Tools** answers `/api/v1/jobs` at `https://img.openvibe.tools`,
2069 `https://audio.openvibe.tools` and `https://docs.openvibe.tools` as well as `origins.tools`
2070 (`platform.toolsOrigins`, `DEFAULT_TOOLS_SATELLITES`, option `toolsSatellites`); a job is found
2071 only on the origin that created it.
2072- **Mock catalog and descriptor** follow openvibe-contracts v0.28.0: `DEFAULT_APP_CATALOG` is its
2073 public + active capabilities (adds `events.app.*`, `codes.release.*`, `community.*`, `vip.*`),
2074 and the default `contractsVersion` is 0.28.0.
2075- Mock state: `state.events[]` entries carry `project_id` and `env`, `state.mediaTenants` is new,
2076 checkpoints are stored as `{ cursor, updated_at }`; `publishEvent()` stores an event of publisher
2077 `app:<id>` as that registered app's (project and env), or takes `{ projectId, env }`.
2078
2079## 0.3.0 (2026-09-23)
2080
2081Developer apps (Network developer projects, ADR-014) and the gaps OpenVibe.Examples found.
2082Contract types re-copied from openvibe-contracts v0.26.0 (`[email protected]`:
2083`project_id`, `env`, `on_behalf_of`); CI checks them against that tag.
2084
2085- **Auth, developer apps.** `exchangeCode()` sends `audience` and `scope`, supports public clients
2086 (no `clientSecret`; the PKCE `codeVerifier` is then required, and always for `app_…` clients)
2087 and returns app tokens as they come (no `refresh_token`). New `verifyAppToken(token, { jwks |
2088 publicKey, issuer, audience, acceptSandbox })`: RS256, expiry, issuer, the required audience,
2089 the claim shape, and `env: sandbox` refused (`token.sandbox_refused`) unless `acceptSandbox`;
2090 returns the claims including `project_id`, `env` and `on_behalf_of`. `verifyUserToken()` is
2091 unchanged and still refuses app tokens (`token.not_user`).
2092- **Authorize URL.** `buildAuthorizeUrl()` / `startAuthorization()` take `audience`; `scope` takes
2093 capability ids. **Behaviour change:** the `'profile theme'` default now applies only when no
2094 `audience` is given.
2095- **Token client.** `createServiceTokenClient().getTokenInfo(ctx)` returns the granted `scope`, the
2096 expiry and `unverifiedClaims` (decoded, NOT verified). New `decodeUnverified()` /
2097 `unverifiedClaims()` helpers in `openvibe-sdk/auth` (browser build too).
2098- **Core.** `client.request({ responseType: 'response' })` now returns the raw fetch `Response`
2099 (as `data` and `response`, body unread) instead of `data: null`; error statuses are still read
2100 and thrown, the per-attempt timeout covers the headers only, and `signal` still cancels the body.
2101 `core.SDK_VERSION` is the package version again (it said 0.1.0 in 0.2.x); a test keeps them equal.
2102- **Realtime.** `parseSSE(body, { onRetry })` is exported: an async iterator of
2103 `{ event, data, id }` over a fetch body or any async iterable.
2104- **Events.** `iterate()` calls `onPage(page)` after every item of that page was yielded and
2105 handled (it used to run before the page's items), so saving `next_after_seq` there is
2106 crash-safe; a break or throw inside the page leaves the previous cursor. New `maxPages` option.
2107 **Behaviour change** for anyone who relied on the old order.
2108- **New `openvibe-sdk/jobs`** (browser-safe): OpenVibe.Tools jobs — `submit()` (always with an
2109 `Idempotency-Key`; JSON or multipart with files), `get()`, `cancel()`, `events()` (SSE that
2110 reattaches with `Last-Event-ID` after drops and restarts, each event once), `wait()`, `file()`
2111 (raw Response).
2112- **New `openvibe-sdk/projects`** (browser-safe; needs a Network user token): the
2113 `/api/v1/projects` API — projects, members, apps, credentials rotate/revoke, grants
2114 request/approve/deny/revoke, quotas, audit, the catalog. Calls that mint a secret or that
2115 Network does not dedupe are never retried.
2116- **Browser bundle.** `browser/openvibe-sdk.mjs` (export `openvibe-sdk/browser/openvibe-sdk.mjs`):
2117 one self-contained ES module of the browser-safe subpaths for pages with no build step,
2118 generated by `scripts/browser-bundle.js` (no dependencies) from the CommonJS sources, checked
2119 in, with a staleness test, and covered by the browser secret scan.
2120- **Mock platform.** Developer apps (`apps`, `projects`, `addApp()`, `signAppToken()`):
2121 confidential and public app clients, app tokens with `project_id`/`env`/`ns`/`on_behalf_of`,
2122 `GET /oauth/authorize` with auto-consent (`setAuthorization()`), audience-bound codes, PKCE
2123 required for apps, sandbox membership, `sandboxAudiences`; mock services refuse sandbox tokens
2124 unless `acceptSandbox` names the audience or capability; a Media tenant per project; the
2125 `/api/v1/projects` API. Events: `pruneEvents()` so pulls and realtime resumes report retention
2126 gaps; `deliverEvents()` / `startDeliveries()` play the delivery worker (signed POSTs to a local
2127 endpoint, in order, retries, dead after `max_attempts`). Optional Tools jobs (`jobs: true |
2128 { stepMs, handlers }`): submit with idempotency replay, get, cancel, SSE with `Last-Event-ID`,
2129 204 when finished, result files, `dropJobStreams()`. Fixed: a realtime resume with a cursor
2130 ahead of the stream no longer swallows the next events. Default `contractsVersion` 0.26.0; a
2131 `tools` origin. Not included: Chat (no WebSocket mock; OpenVibe.Examples keeps its own).
2132
2133## 0.2.2 (2026-09-23)
2134
2135- No peer dependency on `openvibe-contracts`: the SDK never loads it at runtime, and a peer pinned
2136 to one Contracts tarball made npm refuse any service on a newer Contracts release. The supported
2137 Contracts range is documented in the README and checked by CI.
2138
2139## 0.2.1 (2026-09-23)
2140
2141- Outbox: a refusal from the Network token endpoint (for example a grant not provisioned yet) is
2142 retried, never treated as Events rejecting the envelope.
2143
2144## 0.2.0 (2026-09-23)
2145
2146- `openvibe-sdk/events`: `createOutbox(db, { events })` and `createInbox(db)` — the transactional
2147 outbox and exactly-once inbox on the service's own better-sqlite3 handle (ADR-004), so producers
2148 no longer need the `openvibe-events` server package. `enqueue()` refuses to run outside a
2149 transaction; the relay batches, backs off on transient failures and rejects only the row a
2150 permanent 4xx refused. Still no runtime dependencies (`better-sqlite3` is a dev dependency).
2151
2152## 0.1.0 (2026-09-22)
2153
2154First release (roadmap Wave 2, implementation plan §3.2). Built and tested against
2155openvibe-contracts `>=0.5.0 <1.0.0` (v0.6.0).
2156
2157- `openvibe-sdk/core`: `createClient()` with per-attempt timeouts and a whole-call deadline;
2158 retries only for idempotent methods or requests carrying an `Idempotency-Key` (generated for
2159 mutations when `retries > 0`, opt out with `idempotencyKey: false`); `Retry-After`; one token
2160 refresh on 401; RFC 9457 problems as `OpenVibeError` (`code`, `status`, `detail`, `requestId`,
2161 `traceId`); W3C `traceparent` + `X-OpenVibe-Request-Id` on every call, `withContext()` /
2162 `fromRequest()` to continue an incoming trace; `paginate()` async iterator; `discover()` reads
2163 `/.well-known/openvibe`, caches service origins and checks the contracts version.
2164- `openvibe-sdk/auth`: server `createServiceTokenClient()` (client credentials, cache per audience
2165 until 60 s before expiry, shared in-flight request), `verifyUserToken()` (offline RS256 against
2166 the Network JWKS), `exchangeCode()` / `refreshUserToken()`; browser PKCE helpers
2167 (`startAuthorization()`, `buildAuthorizeUrl()`, `readCallback()`).
2168- `openvibe-sdk/registry`: services, service, capabilities, capability, namespaces, contracts,
2169 topics, domain.
2170- `openvibe-sdk/identity` (server): `resolve()`, `resolveBatch()` (split at 500).
2171- `openvibe-sdk/modules`: user get/put/delete/list/publicGet with `If-Match`, `update()`
2172 read-modify-write retrying on 412; service `forSubject.get/put/update`.
2173- `openvibe-sdk/events` (server): `publish()` fills `event_id`, `timestamp`, `source`, `version`,
2174 `payload`, `trace_id`; `pull()` / `iterate()` with cursor and gap reporting; checkpoints;
2175 subscriptions; deliveries/replay; `verifyDelivery()`, `signDelivery()`, `parseDelivery()`.
2176- `openvibe-sdk/realtime`: `subscribe()` over SSE (EventSource or fetch stream), resume from the
2177 last seq, dedupe, `onGap`.
2178- `openvibe-sdk/media`: files upload/list/iterate/get/delete on `/api/v1/:app/files`, public URL
2179 helpers.
2180- `openvibe-sdk/community`: pastes list/iterate/get/create (text or screenshot)/update/delete/fork/
2181 like/copy/versions/byUser/config and paste comments, with `actingSubject`, `origin: 'ai'`,
2182 `sourceRef` and `staff` for services.
2183- `openvibe-sdk/testing`: `createMockPlatform()`, an in-process fake Network (token endpoint,
2184 JWKS, discovery, registry, modules, identity), Events (publish, pull, subscriptions, SSE) and
2185 Media files.
2186- Hand-written `.d.ts` per subpath; ESM entry points; MIT license.
2187