openvibers/openvibe.sdk Commits
v0.22.0: openvibe-sdk/placement (the adaptive fabric's planner: constraints before cost, marginal cost after free allowances with forecast and reserve, owned-capacity spill-over, hysteresis, rendezvous hashing, power of two choices, Ed25519-signed route plans with a last-good holder) and openvibe-sdk/govern (weighted cost units, tiered windowed quotas, reserve/commit/release settled by any process, idempotent charges, concurrency leases, on Valkey or in memory).
Changes
@@@ -8,6 +8,11 @@ release may change an API and says so here.++## 0.22.0 (2026-09-28)++++- **`openvibe-sdk/placement`** (roadmap WS-Z9, the adaptive fabric): `plan(requirements, offers, { rateCards, states, current })` places a workload on the best eligible node or provider and explains it (platform.placement-result@1). Hard constraints first (capabilities, trust, residency, health, latency ceiling, capacity, cost ceiling; excluded candidates carry the reason), then the objective (cheapest, lowest-latency, balanced; private and first-party-only never leave first-party capacity; critical work stays with its authority). `marginalCost` prices work in a provider's real billing unit after its free allowance, forecast to the end of the period, with a reserve kept for high-priority work; owned capacity is ~free until its binding resource is busy, then work spills over. Hysteresis keeps a placement unless an alternative is clearly better (15% by default); failover is immediate. `rendezvous` (weighted highest-random-weight hashing), `pickTwo` (power of two choices), and Ed25519-signed route plans: `signPlan`, `verifyPlan`, `createPlanHolder` (keeps the last valid plan when a new one is bad, expired or older).++- **`openvibe-sdk/govern`** (roadmap WS-Z1): weighted cost units (common.resource-cost@1) with quotas per subject and tier over minute, hour, day and month windows; `reserve` → `commit(actual)` or `release`, settled by any process; idempotency keys so a retried charge counts once; concurrency leases. Valkey holds the counters (atomic Lua); without Valkey they live in the process.++## 0.21.3 (2026-09-28)- `openvibe-sdk/testing` `createTestDb`: on the containers it also returns `url` and `directUrl`, the `DATABASE_URL` and `DATABASE_DIRECT_URL` of its schema, for a test that spawns a process of the service (OpenRe's transport workers); `null` on PGlite.@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.21.3",++ "version": "0.22.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.21.3, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.22.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.21.3';++const SDK_VERSION = '0.22.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/govern.js. Do not edit.++import cjs from '../src/govern.js';++++export const {++ createGovernor, WINDOWS, UNITS,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,8 @@++// Generated by scripts/esm.js from src/placement.js. Do not edit.++import cjs from '../src/placement.js';++++export const {++ plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost,++ rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical,++} = cjs;++export default cjs;@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.21.3",++ "version": "0.22.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.21.3",++ "version": "0.22.0","license": "MIT","devDependencies": {"@electric-sql/pglite": "^0.5.8",@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.21.3",++ "version": "0.22.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -76,6 +76,18 @@"import": "./esm/limits.mjs","default": "./src/limits.js"},++ "./govern": {++ "types": "./types/govern.d.ts",++ "browser": null,++ "import": "./esm/govern.mjs",++ "default": "./src/govern.js"++ },++ "./placement": {++ "types": "./types/placement.d.ts",++ "browser": null,++ "import": "./esm/placement.mjs",++ "default": "./src/placement.js"++ },"./db": {"types": "./types/db.d.ts","browser": null,@@@ -25,6 +25,8 @@ const ENTRIES = {'community.mjs': '../src/community.js','search.mjs': '../src/search.js','limits.mjs': '../src/limits.js',++ 'govern.mjs': '../src/govern.js',++ 'placement.mjs': '../src/placement.js','db.mjs': '../src/db/index.js','valkey.mjs': '../src/valkey.js','cache.mjs': '../src/cache.js',@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.21.3';++const SDK_VERSION = '0.22.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,192 @@++'use strict';++/**++ * openvibe-sdk/govern — one resource-governance layer for every product (roadmap WS-Z1, decision 40): weighted cost++ * units (common.resource-cost@1), quotas per subject and tier over several windows, reserve → commit | release so++ * a failed job gives its cost back, idempotency keys so a retried charge counts once, and concurrency leases.++ * Valkey holds the counters across processes and hosts; without Valkey they live in this process (tests, one++ * process).++ *++ * const gov = createGovernor({ policy, valkey, now });++ * const r = await gov.reserve({ subject: 'user:usr_…', tier: 'user', unit: 'browser-second', amount: 30, key: 'run_1:step_2' });++ * if (!r.ok) → 429 with r.retryAfterS (r.window, r.limit, r.used)++ * await gov.commit(r.id, 12.5); // the real amount; the difference goes back++ * await gov.release(r.id); // the work never happened++ * const lease = await gov.lease({ subject, kind: 'browser', max: 2, ttlMs: 60000 }); // { ok, id, release() }++ *++ * policy: { [unit]: { [tier]: { minute?, hour?, day?, month? } } } — a missing tier or window is unlimited, 0++ * refuses. Contracts publishes the numbers so pages and the API read the same ones (WS-Z1 task 5).++ */++const crypto = require('crypto');++++const WINDOWS = { minute: 60e3, hour: 3600e3, day: 86400e3, month: 30 * 86400e3 };++const UNITS = ['browser-second', 'ai-token', 'ai-usd', 'gpu-second', 'video-minute', 'bandwidth-byte', 'storage-byte-day', 'event',++ 'watch-check', 'bot-control-second', 'job-run', 'upload-byte', 'download-byte'];++++function windowsFor(policy, unit, tier, t) {++ const p = (policy[unit] && policy[unit][tier]) || null;++ if (!p) return [];++ return Object.keys(WINDOWS).filter((w) => p[w] != null).map((w) => {++ const len = WINDOWS[w]; const start = Math.floor(t / len) * len;++ return { w, limit: Number(p[w]), start, len, ttlMs: start + len - t };++ });++}++++// ── Stores ───────────────────────────────────────────────────────────────────++++function memoryStore() {++ const counters = new Map(); const reservations = new Map(); const leases = new Map();++ const sweep = (t) => { for (const [k, v] of counters) if (v.until <= t) counters.delete(k); for (const [k, v] of reservations) if (v.until <= t) reservations.delete(k); };++ return {++ async reserve(id, keys, amount, ttlMs, t) {++ sweep(t);++ if (reservations.has(id)) return { ok: true, replay: true, reservation: reservations.get(id) };++ for (const k of keys) {++ const c = counters.get(k.key);++ if ((c ? c.used : 0) + amount > k.limit) return { ok: false, window: k.w, limit: k.limit, used: c ? c.used : 0, retryMs: k.ttlMs };++ }++ for (const k of keys) {++ const c = counters.get(k.key) || { used: 0, until: t + k.ttlMs };++ c.used += amount; counters.set(k.key, c);++ }++ const r = { amount, keys: keys.map((k) => k.key), until: t + ttlMs };++ reservations.set(id, r);++ return { ok: true, reservation: r };++ },++ async settle(id, actual, t) {++ sweep(t);++ const r = reservations.get(id);++ if (!r) return false;++ const delta = actual == null ? -r.amount : actual - r.amount;++ for (const key of r.keys) { const c = counters.get(key); if (c) c.used = Math.max(0, c.used + delta); }++ reservations.delete(id);++ return true;++ },++ async used(key, t) { sweep(t); const c = counters.get(key); return c ? c.used : 0; },++ async lease(key, id, max, ttlMs, t) {++ const m = leases.get(key) || new Map();++ for (const [lid, until] of m) if (until <= t) m.delete(lid);++ if (m.size >= max) return false;++ m.set(id, t + ttlMs); leases.set(key, m);++ return true;++ },++ async unlease(key, id) { const m = leases.get(key); if (m) m.delete(id); },++ };++}++++const RESERVE = `++-- KEYS: reservation key, then one counter key per window. ARGV: amount, reservation ttl ms, then limit, ttl ms per window.++if redis.call('EXISTS', KEYS[1]) == 1 then return {1, 1} end++local amount = tonumber(ARGV[1])++for i = 2, #KEYS do++ local used = tonumber(redis.call('GET', KEYS[i]) or '0')++ local limit = tonumber(ARGV[3 + (i - 2) * 2])++ if used + amount > limit then return {0, i - 1, tostring(used), ARGV[4 + (i - 2) * 2]} end++end++for i = 2, #KEYS do++ redis.call('INCRBYFLOAT', KEYS[i], amount)++ redis.call('PEXPIRE', KEYS[i], tonumber(ARGV[4 + (i - 2) * 2]) + 5000)++end++local keys = {}++for i = 2, #KEYS do keys[#keys + 1] = KEYS[i] end++redis.call('SET', KEYS[1], cjson.encode({ amount = amount, keys = keys }), 'PX', tonumber(ARGV[2]))++return {1, 0}`;++const SETTLE = `++-- KEYS: reservation key. ARGV: the actual amount, or '' to release it all. Moves each counter the reservation++-- charged by (actual - reserved), never below 0, and ends the reservation. Any process may settle it.++local raw = redis.call('GET', KEYS[1])++if not raw then return 0 end++local r = cjson.decode(raw)++local delta = -r.amount++if ARGV[1] ~= '' then delta = tonumber(ARGV[1]) - r.amount end++redis.call('DEL', KEYS[1])++for _, k in ipairs(r.keys) do++ local v = tonumber(redis.call('INCRBYFLOAT', k, delta))++ if v < 0 then redis.call('SET', k, '0', 'KEEPTTL') end++end++return 1`;++const LEASE = `++-- KEYS: lease set. ARGV: id, max, ttl ms, now ms.++redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[4])++if redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[2]) then return 0 end++redis.call('ZADD', KEYS[1], tonumber(ARGV[4]) + tonumber(ARGV[3]), ARGV[1])++redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[3]) + 5000)++return 1`;++++function valkeyStore(valkey) {++ const c = valkey.client;++ if (typeof c.ovGovReserve !== 'function') {++ c.defineCommand('ovGovReserve', { lua: RESERVE });++ c.defineCommand('ovGovSettle', { numberOfKeys: 1, lua: SETTLE });++ c.defineCommand('ovGovLease', { numberOfKeys: 1, lua: LEASE });++ }++ const rkey = (id) => valkey.key('gov', 'res', id);++ return {++ async reserve(id, keys, amount, ttlMs) {++ const args = [String(amount), String(ttlMs)];++ for (const k of keys) args.push(String(k.limit), String(Math.max(1, Math.ceil(k.ttlMs))));++ const r = await c.ovGovReserve(1 + keys.length, rkey(id), ...keys.map((k) => valkey.key('gov', k.key)), ...args);++ if (Number(r[0]) === 1) return { ok: true, replay: Number(r[1]) === 1 };++ const k = keys[Number(r[1]) - 1];++ return { ok: false, window: k.w, limit: k.limit, used: Number(r[2]), retryMs: Number(r[3]) };++ },++ async settle(id, actual) { return Number(await c.ovGovSettle(rkey(id), actual == null ? '' : String(actual))) === 1; },++ async used(key) { return Number((await c.get(valkey.key('gov', key))) || 0); },++ async lease(key, id, max, ttlMs, t) { return Number(await c.ovGovLease(valkey.key('gov', 'lease', key), id, String(max), String(ttlMs), String(t))) === 1; },++ async unlease(key, id) { await c.zrem(valkey.key('gov', 'lease', key), id); },++ };++}++++// ── The governor ─────────────────────────────────────────────────────────────++++function createGovernor({ policy = {}, valkey = null, now = () => Date.now(), reservationTtlMs = 6 * 3600e3, onRefused = null, onUsage = null } = {}) {++ const store = valkey ? valkeyStore(valkey) : memoryStore();++++ async function reserve({ subject, tier = 'user', unit, amount, key, project = null }) {++ if (!UNITS.includes(unit)) throw new TypeError(`govern: unknown unit ${unit}`);++ if (!(amount >= 0)) throw new TypeError('govern: amount must be ≥ 0');++ if (!key || String(key).length < 8) throw new TypeError('govern: an idempotency key of 8+ characters is required');++ const t = now();++ const id = crypto.createHash('sha256').update(`${subject}\u0000${unit}\u0000${key}`).digest('base64url').slice(0, 32);++ const scope = project ? `${project}:${subject}` : subject;++ const keys = windowsFor(policy, unit, tier, t).map((w) => ({ ...w, key: `${unit}:${w.w}:${w.start}:${scope}` }));++ const r = await store.reserve(id, keys, Number(amount), reservationTtlMs, t);++ if (!r.ok) {++ const out = { ok: false, unit, window: r.window, limit: r.limit, used: r.used, retryAfterS: Math.max(1, Math.ceil(r.retryMs / 1000)) };++ if (onRefused) onRefused({ subject, tier, unit, amount, ...out });++ return out;++ }++ if (onUsage && !r.replay) onUsage({ subject, project, unit, amount: Number(amount), state: 'reserved', idempotency_key: key, at: new Date(t).toISOString() });++ return { ok: true, id, replay: Boolean(r.replay) };++ }++++ /** The real amount used: counters move by (actual − reserved); the reservation ends. False when it had already ended. */++ async function commit(id, actual) {++ if (!(actual >= 0)) throw new TypeError('govern: commit needs the actual amount (≥ 0)');++ return await store.settle(id, Number(actual), now());++ }++++ /** The work never happened: everything reserved goes back. */++ async function release(id) {++ return await store.settle(id, null, now());++ }++++ /** Used so far in each window of a unit, for a subject and tier (for dashboards and the limits page). */++ async function usage({ subject, tier = 'user', unit, project = null }) {++ const t = now(); const scope = project ? `${project}:${subject}` : subject;++ const out = {};++ for (const w of windowsFor(policy, unit, tier, t)) out[w.w] = { used: await store.used(`${unit}:${w.w}:${w.start}:${scope}`, t), limit: w.limit };++ return out;++ }++++ /** At most `max` at once for (subject, kind); a lease expires by itself after ttlMs. */++ async function lease({ subject, kind, max, ttlMs = 60e3 }) {++ const id = crypto.randomBytes(9).toString('base64url');++ const key = `${kind}:${subject}`;++ const ok = await store.lease(key, id, max, ttlMs, now());++ return ok ? { ok: true, id, release: () => store.unlease(key, id) } : { ok: false, retryAfterS: Math.max(1, Math.ceil(ttlMs / 1000)) };++ }++++ return { reserve, commit, release, usage, lease, policy };++}++++module.exports = { createGovernor, WINDOWS, UNITS };nnew file mode 100644@@@ -0,0 +1,257 @@++'use strict';++/**++ * openvibe-sdk/placement — where a workload runs (roadmap WS-Z9, decision 43; the event fabric WS-Z3 and the provider++ * router WS-Z10 use the same planner). Pure functions over data, no I/O: callers pass the offers they know++ * (platform.resource-offer@1), the provider rate cards and usage (platform.rate-card@1, platform.provider-state@1)++ * and the workload's requirements (platform.workload-requirements@1).++ *++ * const { plan, marginalCost, rendezvous, pickTwo, signPlan, verifyPlan } = require('openvibe-sdk/placement');++ * const r = plan(requirements, offers, { rateCards, states, now, current }); // platform.placement-result@1++ *++ * The rules:++ * - hard constraints first: capabilities, trust, residency/region, health, latency ceiling, capacity, cost ceiling.++ * A cheaper candidate that misses one is excluded, with the reason, never chosen;++ * - then the objective: `lowest-latency` minimises measured latency within the budget; `cheapest` minimises++ * marginal cost within the latency ceiling; `balanced` weighs both; `correctness` keeps the authority (the++ * candidate named in `requirements.authority`, or the current placement) and never moves for cost;++ * - marginal cost is priced in each provider's real billing unit after its free allowance, forecast to the end of++ * the period and with a reserve kept for higher-priority work; owned (prepaid) capacity costs ~0 until its++ * binding resource is busy, then rises so work spills to free or paid capacity before the node saturates;++ * - hysteresis: a candidate replaces the current placement only when it is clearly better (`minGain`), or when++ * the current one is no longer eligible (failover is immediate).++ */++const crypto = require('crypto');++++const TRUST_ORDER = ['first-party', 'partner', 'community', 'external'];++const HEALTH_OK = new Set(['up', 'degraded']);++++// ── Cost ─────────────────────────────────────────────────────────────────────++++/** Price of `units` in one rate card over a period, after its free allowance: whole billing units (unit_size). */++function priceOf(card, units) {++ const billable = Math.max(0, units - (Number(card.free_allowance) || 0));++ if (!billable) return 0;++ const size = Number(card.unit_size) || 1;++ return (billable / size) * (Number(card.unit_price_usd) || 0); // unit_price_usd is per unit_size, prorated as providers bill++}++++/**++ * Linear forecast of a period's usage: what was used so far, run to the end of the period at the same rate.++ * `start`/`end`/`now` in ms.++ */++function forecast(used, start, end, now) {++ const elapsed = Math.max(1, now - start);++ const total = Math.max(elapsed, end - start);++ return used * (total / elapsed);++}++++/**++ * The marginal cost of `projected` more units on a provider this period: cost(forecast + projected) −++ * cost(forecast), where the free allowance shrinks by the reserve kept for higher-priority work (unless this work++ * is itself high priority). When the forecast says the allowance runs out anyway, the next unit is priced as paid:++ * a free allowance that will not last is not free.++ */++function marginalCost(card, state, projected, { now = Date.now(), priority = 'normal' } = {}) {++ const metric = card.metric;++ const used = Number((state && state.usage && state.usage[metric]) || 0);++ const start = state && state.period_start ? Date.parse(state.period_start) : now;++ const end = state && state.period_end ? Date.parse(state.period_end) : now;++ const expected = state && state.forecast && state.forecast[metric] != null ? Number(state.forecast[metric]) : forecast(used, start, end, now);++ const reserveShare = priority === 'high' ? 0 : Number((state && state.reserve && state.reserve[metric]) || 0);++ const effective = { ...card, free_allowance: (Number(card.free_allowance) || 0) * (1 - reserveShare) };++ return Math.max(0, priceOf(effective, expected + projected) - priceOf(effective, expected));++}++++/**++ * Owned (prepaid) capacity: ~0 while the binding resource is below `busy` (default 0.6), rising steeply to `max`++ * above it, so background work spills elsewhere before the node saturates. Utilisation is the highest of CPU,++ * memory, disk and GPU the offer reports.++ */++function ownedLoadCost(offer, { busy = 0.6, full = 0.9, maxUsd = 0.001 } = {}) {++ const c = offer.capacity || {};++ const u = Math.max(0, ...['cpu', 'memory', 'disk', 'gpu'].map((k) => Number((c[k] && c[k].utilization) || 0)));++ if (u <= busy) return 0;++ if (u >= full) return maxUsd;++ return maxUsd * ((u - busy) / (full - busy)) ** 2;++}++++// ── Eligibility ──────────────────────────────────────────────────────────────++++function latencyOf(offer, op) {++ const l = offer.latency_ms || {};++ if (op && l[op] != null) return Number(l[op]);++ const vals = Object.entries(l).filter(([k]) => /p95/.test(k)).map(([, v]) => Number(v));++ return vals.length ? Math.min(...vals) : null;++}++++/** Why an offer cannot run the workload, or null when it can. */++function excluded(req, offer) {++ const caps = new Set(offer.capabilities || []);++ for (const c of req.capabilities || []) if (!caps.has(c)) return `lacks ${c}`;++ const trust = req.trust && req.trust.length ? req.trust : TRUST_ORDER;++ if (!trust.includes(offer.trust)) return `workload requires ${trust.join(' or ')} trust`;++ if (req.residency && offer.region !== req.residency && !(offer.regions || []).includes(req.residency)) return `outside ${req.residency}`;++ if (req.region && req.region !== 'nearest' && offer.region !== req.region && offer.region !== 'global') return `not in ${req.region}`;++ const h = offer.health || {};++ if (!HEALTH_OK.has(h.status)) return `health ${h.status || 'unknown'}`;++ const lat = latencyOf(offer, req.latency_op);++ if (req.max_latency_ms != null && lat != null && lat > req.max_latency_ms) return `p95 ${lat} ms over ${req.max_latency_ms} ms`;++ const r = req.resources || {};++ const c = offer.capacity || {};++ if (r.cpu && c.cpu && c.cpu.available_cores != null && c.cpu.available_cores < r.cpu) return `${c.cpu.available_cores} cores free, needs ${r.cpu}`;++ if (r.memory_mb && c.memory && c.memory.available_mb != null && c.memory.available_mb < r.memory_mb) return `${c.memory.available_mb} MB free, needs ${r.memory_mb}`;++ if (r.gpu && !(c.gpu && c.gpu.type)) return 'no GPU';++ for (const cap of req.capabilities || []) {++ const kind = /^worker:(.+)$/.exec(cap);++ if (kind && c.workers && c.workers[kind[1]] === 0) return `no free ${kind[1]} worker`;++ }++ return null;++}++++// ── The planner ──────────────────────────────────────────────────────────────++++function estimateCost(req, offer, ctx) {++ const units = Number(req.units || 1);++ if (offer.pricing && offer.pricing.model === 'prepaid') return ownedLoadCost(offer, ctx.owned) * units;++ const card = offer.pricing && offer.pricing.rate_card && ctx.rateCards.get(offer.pricing.rate_card);++ if (card) return marginalCost(card, ctx.states.get(card.provider) || ctx.states.get(offer.provider), units, { now: ctx.now, priority: req.latency_class === 'critical' || req.latency_class === 'realtime' ? 'high' : 'normal' });++ const per = offer.pricing && offer.pricing.marginal_usd_per_unit;++ return per != null ? Number(per) * units : Infinity; // an unpriced paid provider is never assumed free++}++++/**++ * plan(requirements, offers, { rateCards, states, now, current, minGain, weights }) → platform.placement-result@1.++ * rateCards: array or Map by id; states: array or Map by provider (platform.provider-state@1); current: the id++ * the workload runs on now (hysteresis); minGain: relative improvement needed to move (default 0.15).++ */++function plan(req, offers, { rateCards = [], states = [], now = Date.now(), current = null, minGain = 0.15, weights = { cost: 0.5, latency: 0.5 }, owned = {} } = {}) {++ const ctx = {++ now, owned,++ rateCards: rateCards instanceof Map ? rateCards : new Map(rateCards.map((c) => [c.id, c])),++ states: states instanceof Map ? states : new Map(states.map((s) => [s.provider, s])),++ };++ const objective = req.objective || 'balanced';++ // Objectives that are really constraints: private and first-party work never leaves first-party capacity.++ if ((objective === 'first-party-only' || objective === 'private') && !(req.trust && req.trust.length)) req = { ...req, trust: ['first-party'] };++ const candidates = offers.map((o) => {++ const why = excluded(req, o);++ if (why) return { id: o.offer_id, eligible: false, excluded_because: why };++ const cost = estimateCost(req, o, ctx);++ if (req.max_cost_usd != null && cost > req.max_cost_usd) return { id: o.offer_id, eligible: false, excluded_because: `estimated $${cost.toFixed(6)} over $${req.max_cost_usd}` };++ const lat = latencyOf(o, req.latency_op);++ return { id: o.offer_id, eligible: true, estimated_cost_usd: cost, estimated_latency_ms: lat == null ? undefined : lat, trust: o.trust };++ });++ const ok = candidates.filter((c) => c.eligible && Number.isFinite(c.estimated_cost_usd));++ const reasons = [];++ if (!ok.length) return { selected: null, objective, reasons: ['no eligible candidate'], candidates: strip(candidates), decided_at: new Date(now).toISOString() };++++ const maxCost = Math.max(...ok.map((c) => c.estimated_cost_usd), 1e-12);++ const lats = ok.map((c) => c.estimated_latency_ms).filter((x) => x != null);++ const maxLat = Math.max(...lats, 1);++ for (const c of ok) {++ const nc = c.estimated_cost_usd / maxCost;++ const nl = (c.estimated_latency_ms == null ? maxLat : c.estimated_latency_ms) / maxLat;++ c.score = objective === 'cheapest' ? nc + nl * 1e-3++ : objective === 'lowest-latency' ? nl + nc * 1e-3++ : nc * weights.cost + nl * weights.latency;++ c.score = Number(c.score.toFixed(6));++ }++ ok.sort((a, b) => a.score - b.score || (a.estimated_latency_ms ?? Infinity) - (b.estimated_latency_ms ?? Infinity));++ let best = ok[0];++ const cur = current && ok.find((c) => c.id === current);++ if (objective === 'correctness' || req.latency_class === 'critical') {++ const authority = (req.authority && ok.find((c) => c.id === req.authority)) || cur;++ if (authority) { best = authority; reasons.push('the authority keeps critical work'); }++ } else if (cur && cur !== best) {++ const gain = cur.score > 0 ? (cur.score - best.score) / cur.score : 0;++ if (gain < minGain) { best = cur; reasons.push(`stays on ${cur.id}: the best alternative is only ${(gain * 100).toFixed(1)}% better (needs ${(minGain * 100).toFixed(0)}%)`); }++ } else if (current && !cur) reasons.push(`${current} is no longer eligible: moved`);++ if (best.estimated_cost_usd === 0) reasons.push('no marginal cost (already-paid capacity or a free allowance)');++ if (req.max_latency_ms != null) reasons.push(`meets the ${req.max_latency_ms} ms latency ceiling`);++ reasons.push(`objective ${objective}`);++ return { selected: best.id, objective, reasons, candidates: strip(candidates), decided_at: new Date(now).toISOString() };++}++++function strip(cands) {++ return cands.map(({ trust, ...c }) => c); // platform.placement-result@1 candidate shape++}++++// ── Stable assignment and local balancing ────────────────────────────────────++++/** Weighted rendezvous (highest random weight) hashing: the target for `key`; adding or removing one moves little. */++function rendezvous(key, targets) {++ let best = null; let bestScore = -Infinity;++ for (const t of targets) {++ const w = Number(t.weight == null ? 1 : t.weight);++ if (!(w > 0)) continue;++ const h = crypto.createHash('sha256').update(`${t.id}\u0000${key}`).digest();++ const u = (h.readUInt32BE(0) + 1) / 4294967297; // (0, 1)++ const score = -w / Math.log(u);++ if (score > bestScore) { bestScore = score; best = t; }++ }++ return best;++}++++/** Power of two choices: two random candidates, the one with less load (least-request without a thundering herd). */++function pickTwo(items, load, rand = Math.random) {++ if (!items.length) return null;++ if (items.length === 1) return items[0];++ const i = Math.floor(rand() * items.length);++ let j = Math.floor(rand() * (items.length - 1));++ if (j >= i) j++;++ return load(items[j]) < load(items[i]) ? items[j] : items[i];++}++++// ── Signed route plans (platform.placement-plan@1) ───────────────────────────++++function canonical(v) {++ if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;++ if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;++ return JSON.stringify(v);++}++++/** Sign a plan with an Ed25519 private key (PEM or KeyObject): returns the plan with `signature`. */++function signPlan(planBody, privateKey) {++ const { signature, ...body } = planBody;++ const sig = crypto.sign(null, Buffer.from(canonical(body)), privateKey);++ return { ...body, signature: sig.toString('base64url') };++}++++/**++ * Verify a plan: its key id is known, the signature matches, and it has not expired. Returns { ok, reason }.++ * publicKeys: { [key_id]: PEM | KeyObject }. A caller keeps using its last valid plan when a new one fails this.++ */++function verifyPlan(p, publicKeys, { now = Date.now() } = {}) {++ if (!p || !p.signature) return { ok: false, reason: 'unsigned' };++ const key = publicKeys && publicKeys[p.key_id];++ if (!key) return { ok: false, reason: `unknown key ${p.key_id}` };++ const { signature, ...body } = p;++ let good = false;++ try { good = crypto.verify(null, Buffer.from(canonical(body)), key, Buffer.from(signature, 'base64url')); } catch { good = false; }++ if (!good) return { ok: false, reason: 'bad signature' };++ if (Date.parse(p.expires_at) <= now) return { ok: false, reason: 'expired' };++ return { ok: true };++}++++/**++ * A holder for the current route plan: accept() takes a newer valid plan and keeps the old one otherwise, so a++ * control-plane outage or a bad plan never stops delivery (decision 42).++ */++function createPlanHolder(publicKeys, { now = () => Date.now(), onReject = null } = {}) {++ let current = null;++ return {++ get: () => current,++ accept(p) {++ const v = verifyPlan(p, publicKeys, { now: now() });++ if (!v.ok || (current && p.epoch <= current.epoch)) {++ if (onReject) onReject(v.ok ? `epoch ${p.epoch} is not newer than ${current.epoch}` : v.reason);++ return false;++ }++ current = p;++ return true;++ },++ targets(route) { const r = current && current.routes.find((x) => x.route === route); return r ? r.targets : null; },++ };++}++++module.exports = { plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical };@@@ -76,7 +76,7 @@ run([for (const [re, why] of FORBIDDEN) if (re.test(src)) problems.push(`${path.relative(ROOT, file)}: ${why}`);}assert.deepEqual(problems, []);-- assert.deepEqual(blocked.sort(), ['./cache', './db', './events', './identity', './limits', './openre', './pubsub', './queue', './testing', './valkey', './vip']);++ assert.deepEqual(blocked.sort(), ['./cache', './db', './events', './govern', './identity', './limits', './openre', './placement', './pubsub', './queue', './testing', './valkey', './vip']);const rel = [...scanned].map((f) => path.relative(ROOT, f));for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {assert.ok(rel.includes(must), `${must} is scanned`);nnew file mode 100644@@@ -0,0 +1,77 @@++'use strict';++/** openvibe-sdk/govern: quotas by tier and window, reserve/commit/release, idempotent charges, concurrency leases; in memory and on Valkey. */++const assert = require('node:assert/strict');++const crypto = require('node:crypto');++const { run } = require('./helpers');++const { createGovernor } = require('../src/govern');++const { createValkey } = require('../src/valkey');++++const policy = { 'browser-second': { anonymous: { day: 0 }, user: { minute: 100, day: 600 } }, 'job-run': { user: { hour: 3 } } };++const quiet = { log() {}, warn() {}, error() {} };++++function suite(label, make) {++ return [++ [`${label}: a reservation counts against every window; over the limit is refused with a retry time`, async () => {++ const gov = await make();++ const a = await gov.reserve({ subject: 'user:u1', unit: 'browser-second', amount: 60, key: 'run-1:step-1' });++ assert.equal(a.ok, true);++ const b = await gov.reserve({ subject: 'user:u1', unit: 'browser-second', amount: 50, key: 'run-1:step-2' });++ assert.equal(b.ok, false);++ assert.equal(b.window, 'minute');++ assert.equal(b.limit, 100);++ assert.ok(b.retryAfterS >= 1 && b.retryAfterS <= 60);++ const u = await gov.usage({ subject: 'user:u1', unit: 'browser-second' });++ assert.equal(u.minute.used, 60);++ assert.equal(u.day.limit, 600);++ }],++ [`${label}: commit moves the counters to the real amount; release gives everything back`, async () => {++ const gov = await make();++ const a = await gov.reserve({ subject: 'user:u2', unit: 'browser-second', amount: 90, key: 'run-2:step-1' });++ await gov.commit(a.id, 30);++ assert.equal((await gov.usage({ subject: 'user:u2', unit: 'browser-second' })).minute.used, 30);++ const b = await gov.reserve({ subject: 'user:u2', unit: 'browser-second', amount: 70, key: 'run-2:step-2' });++ assert.equal(b.ok, true);++ await gov.release(b.id);++ assert.equal((await gov.usage({ subject: 'user:u2', unit: 'browser-second' })).minute.used, 30);++ assert.equal(await gov.release(b.id), false, 'settling twice changes nothing');++ }],++ [`${label}: the same idempotency key charges once`, async () => {++ const gov = await make();++ const a = await gov.reserve({ subject: 'user:u3', unit: 'job-run', amount: 1, key: 'job_01J-retry' });++ const again = await gov.reserve({ subject: 'user:u3', unit: 'job-run', amount: 1, key: 'job_01J-retry' });++ assert.equal(again.replay, true);++ assert.equal(again.id, a.id);++ assert.equal((await gov.usage({ subject: 'user:u3', unit: 'job-run' })).hour.used, 1);++ }],++ [`${label}: a zero limit refuses (anonymous browser time); a missing tier is unlimited`, async () => {++ const gov = await make();++ assert.equal((await gov.reserve({ subject: 'ip:1', tier: 'anonymous', unit: 'browser-second', amount: 1, key: 'anon-try-1' })).ok, false);++ assert.equal((await gov.reserve({ subject: 'user:u4', tier: 'vip', unit: 'browser-second', amount: 1e6, key: 'vip-run-1' })).ok, true);++ await assert.rejects(() => gov.reserve({ subject: 'user:u4', unit: 'coins', amount: 1, key: 'abcdefgh' }), /unknown unit/);++ await assert.rejects(() => gov.reserve({ subject: 'user:u4', unit: 'job-run', amount: 1, key: 'k' }), /idempotency key/);++ }],++ [`${label}: concurrency leases cap parallel work and free a slot on release`, async () => {++ const gov = await make();++ const a = await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 });++ const b = await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 });++ const c = await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 });++ assert.deepEqual([a.ok, b.ok, c.ok], [true, true, false]);++ await a.release();++ assert.equal((await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 })).ok, true);++ }],++ ];++}++++const tests = suite('memory', async () => createGovernor({ policy, now: () => Date.parse('2026-09-28T12:00:30Z') }));++const URL = process.env.OV_TEST_VALKEY_URL;++if (!URL) console.log('govern on valkey: skipped (OV_TEST_VALKEY_URL not set; scripts/test-services.sh up)');++else {++ const opened = [];++ tests.push(...suite('valkey', async () => {++ const v = createValkey({ url: URL, prefix: `ovsdk:gov:${crypto.randomBytes(3).toString('hex')}:`, log: quiet });++ opened.push(v);++ return createGovernor({ policy, valkey: v, now: () => Date.parse('2026-09-28T12:00:30Z') });++ }));++ tests.push(['valkey: close', async () => { for (const v of opened) await v.close(); }]);++}++run(tests);@@@ -66,7 +66,7 @@ run([})().catch((err) => { console.error(err); process.exit(1); });`;const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));-- assert.equal(subpaths.length, 28);++ assert.equal(subpaths.length, 30);assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');} finally {fs.rmSync(tmp, { recursive: true, force: true });nnew file mode 100644@@@ -0,0 +1,92 @@++'use strict';++/** openvibe-sdk/placement: constraints before cost, marginal cost after free allowances, hysteresis, rendezvous, P2C, signed plans. */++const assert = require('node:assert/strict');++const crypto = require('node:crypto');++const { run } = require('./helpers');++const { plan, marginalCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, ownedLoadCost } = require('../src/placement');++++const T = Date.parse('2026-09-16T00:00:00Z'); // half of September gone++const period = { period_start: '2026-09-01T00:00:00Z', period_end: '2026-10-01T00:00:00Z' };++const node = (id, util, extra = {}) => ({ offer_id: id, kind: 'node', region: 'us-west', trust: 'first-party', capabilities: ['worker:browser'], capacity: { cpu: { utilization: util, available_cores: 4 }, memory: { available_mb: 8000 } }, latency_ms: { start_p95: 40 }, health: { status: 'up' }, pricing: { model: 'prepaid' }, updated_at: '2026-09-15T00:00:00Z', ...extra });++const provider = (id, card, extra = {}) => ({ offer_id: id, kind: 'provider', provider: id, region: 'global', trust: 'external', capabilities: ['worker:browser'], latency_ms: { start_p95: 120 }, health: { status: 'up' }, pricing: { model: 'per-operation', rate_card: card }, updated_at: '2026-09-15T00:00:00Z', ...extra });++const card = (id, provider, free, price, size = 1e6) => ({ id, provider, metric: 'op', unit_size: size, unit_price_usd: price, free_allowance: free, reset_period: 'month', effective_from: '2026-09-01', source: 'https://example.com', verified_at: '2026-09-28' });++const req = (o = {}) => ({ kind: 'watch.browser', mobility: 'job', latency_class: 'background', objective: 'cheapest', capabilities: ['worker:browser'], ...o });++++run([++ ['a free allowance that will run out before the period ends is priced as paid', async () => {++ const c = card('rc', 'p', 1e6, 0.4);++ // Half the month gone, 430k used: the forecast is 860k, so 100k more stay free.++ assert.equal(marginalCost(c, { provider: 'p', ...period, usage: { op: 430000 } }, 100000, { now: T }), 0);++ // 600k used by mid-month: forecast 1.2M, past the allowance; the next million is billed.++ assert.ok(marginalCost(c, { provider: 'p', ...period, usage: { op: 600000 } }, 1e6, { now: T }) > 0);++ // A 20% reserve for higher-priority work makes normal work pay sooner, never high-priority work.++ const s = { provider: 'p', ...period, usage: { op: 430000 }, reserve: { op: 0.2 } };++ assert.ok(marginalCost(c, s, 100000, { now: T }) > 0);++ assert.equal(marginalCost(c, s, 100000, { now: T, priority: 'high' }), 0);++ }],++ ['owned capacity is ~free until its binding resource is busy, then work spills elsewhere', async () => {++ assert.equal(ownedLoadCost(node('a', 0.3)), 0);++ assert.ok(ownedLoadCost(node('a', 0.8)) > 0);++ const cards = [card('rc_q', 'q', 1e6, 0.4)];++ const states = [{ provider: 'q', ...period, usage: { op: 1000 } }];++ assert.equal(plan(req(), [node('own', 0.3), provider('q', 'rc_q')], { rateCards: cards, states, now: T }).selected, 'own');++ assert.equal(plan(req(), [node('own', 0.95), provider('q', 'rc_q')], { rateCards: cards, states, now: T }).selected, 'q');++ }],++ ['hard constraints exclude a cheaper candidate, with the reason', async () => {++ const r = plan(req({ objective: 'balanced', trust: ['first-party'], max_latency_ms: 100 }), [node('own', 0.2), provider('cheap', 'rc'), node('slow', 0.1, { offer_id: 'slow', latency_ms: { start_p95: 400 } })], { rateCards: [card('rc', 'cheap', 1e9, 0)], now: T });++ assert.equal(r.selected, 'own');++ assert.match(r.candidates.find((c) => c.id === 'cheap').excluded_because, /first-party/);++ assert.match(r.candidates.find((c) => c.id === 'slow').excluded_because, /over 100 ms/);++ assert.equal(plan(req({ objective: 'private' }), [provider('ext', 'rc')], { rateCards: [card('rc', 'ext', 1e9, 0)], now: T }).selected, null, 'private work never leaves first-party capacity');++ assert.match(plan(req(), [node('down', 0.1, { health: { status: 'down' } })], { now: T }).candidates[0].excluded_because, /health down/);++ }],++ ['an unpriced paid provider is never assumed free', async () => {++ const r = plan(req(), [provider('mystery', 'rc_missing')], { now: T });++ assert.equal(r.selected, null);++ }],++ ['hysteresis: stay unless clearly better; fail over at once when the current one drops out', async () => {++ const a = node('a', 0.3, { latency_ms: { start_p95: 50 } }); const b = node('b', 0.3, { latency_ms: { start_p95: 46 } });++ const r = plan(req({ objective: 'lowest-latency' }), [a, b], { current: 'a', now: T });++ assert.equal(r.selected, 'a');++ assert.match(r.reasons[0], /only .*% better/);++ const moved = plan(req({ objective: 'lowest-latency' }), [{ ...a, health: { status: 'down' } }, b], { current: 'a', now: T });++ assert.equal(moved.selected, 'b');++ assert.ok(moved.reasons.some((x) => /no longer eligible/.test(x)));++ }],++ ['critical work stays with its authority whatever the cost', async () => {++ const r = plan(req({ latency_class: 'critical', objective: 'correctness', authority: 'ledger' }), [node('ledger', 0.95), node('idle', 0.1)], { now: T });++ assert.equal(r.selected, 'ledger');++ }],++ ['rendezvous hashing is stable and moves little when a target is added', async () => {++ const t3 = [{ id: 'a', weight: 1 }, { id: 'b', weight: 1 }, { id: 'c', weight: 1 }];++ const keys = Array.from({ length: 2000 }, (_, i) => `room:${i}`);++ const before = keys.map((k) => rendezvous(k, t3).id);++ assert.deepEqual(keys.map((k) => rendezvous(k, t3).id), before);++ const after = keys.map((k) => rendezvous(k, [...t3, { id: 'd', weight: 1 }]).id);++ const moved = after.filter((x, i) => x !== before[i]);++ assert.ok(moved.every((x) => x === 'd'), 'only keys that now belong to the new target move');++ assert.ok(moved.length > 300 && moved.length < 700, `about a quarter move (${moved.length})`);++ const heavy = keys.map((k) => rendezvous(k, [{ id: 'a', weight: 3 }, { id: 'b', weight: 1 }]).id).filter((x) => x === 'a').length;++ assert.ok(heavy > 1300 && heavy < 1700, `weights hold (${heavy}/2000)`);++ }],++ ['power of two choices picks the less loaded of two', async () => {++ const load = { a: 400, b: 120, c: 80 };++ let r = 0; const seq = [0.1, 0.9]; // picks index 0 and then 2++ assert.equal(pickTwo(['a', 'b', 'c'], (x) => load[x], () => seq[r++]), 'c');++ }],++ ['signed route plans: verified, refused when tampered, expired or older; the last good plan stays', async () => {++ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');++ const body = { epoch: 2, issued_at: '2026-09-28T00:00:00Z', expires_at: '2026-09-29T00:00:00Z', issuer: 'network', key_id: 'k1', routes: [{ route: 'events:background', targets: [{ id: 'valkey', weight: 1 }] }] };++ const p = signPlan(body, privateKey);++ const now = Date.parse('2026-09-28T12:00:00Z');++ assert.equal(verifyPlan(p, { k1: publicKey }, { now }).ok, true);++ assert.equal(verifyPlan({ ...p, routes: [] }, { k1: publicKey }, { now }).reason, 'bad signature');++ assert.equal(verifyPlan(p, { k1: publicKey }, { now: Date.parse('2026-09-30T00:00:00Z') }).reason, 'expired');++ assert.equal(verifyPlan(p, { other: publicKey }, { now }).reason, 'unknown key k1');++ const holder = createPlanHolder({ k1: publicKey }, { now: () => now });++ assert.equal(holder.accept(p), true);++ assert.equal(holder.accept(signPlan({ ...body, epoch: 1 }, privateKey)), false, 'an older epoch is ignored');++ assert.equal(holder.accept({ ...p, epoch: 3 }), false, 'a tampered plan is ignored');++ assert.deepEqual(holder.targets('events:background'), [{ id: 'valkey', weight: 1 }]);++ }],++]);nnew file mode 100644@@@ -0,0 +1,19 @@++import type { Valkey } from './valkey';++export type CostUnit = 'browser-second' | 'ai-token' | 'ai-usd' | 'gpu-second' | 'video-minute' | 'bandwidth-byte' | 'storage-byte-day' | 'event'++ | 'watch-check' | 'bot-control-second' | 'job-run' | 'upload-byte' | 'download-byte';++export type Windowed = { minute?: number; hour?: number; day?: number; month?: number };++/** { [unit]: { [tier]: { minute?, hour?, day?, month? } } }: a missing tier or window is unlimited, 0 refuses. */++export type GovernPolicy = Partial<Record<CostUnit, Record<string, Windowed>>>;++export type Reserved = { ok: true; id: string; replay: boolean } | { ok: false; unit: CostUnit; window: string; limit: number; used: number; retryAfterS: number };++export interface Governor {++ reserve(o: { subject: string; tier?: string; unit: CostUnit; amount: number; key: string; project?: string | null }): Promise<Reserved>;++ commit(id: string, actual: number): Promise<boolean>;++ release(id: string): Promise<boolean>;++ usage(o: { subject: string; tier?: string; unit: CostUnit; project?: string | null }): Promise<Record<string, { used: number; limit: number }>>;++ lease(o: { subject: string; kind: string; max: number; ttlMs?: number }): Promise<{ ok: true; id: string; release(): Promise<void> } | { ok: false; retryAfterS: number }>;++ policy: GovernPolicy;++}++export declare function createGovernor(opts?: { policy?: GovernPolicy; valkey?: Valkey | null; now?: () => number; reservationTtlMs?: number;++ onRefused?: (e: object) => void; onUsage?: (e: object) => void }): Governor;++export declare const WINDOWS: Record<'minute' | 'hour' | 'day' | 'month', number>;++export declare const UNITS: CostUnit[];nnew file mode 100644@@@ -0,0 +1,26 @@++/** platform.* contract shapes are validated by openvibe-contracts; these are the fields the planner reads. */++export type Offer = { offer_id: string; kind: 'node' | 'provider'; provider?: string; region: string; regions?: string[]; trust: 'first-party' | 'partner' | 'community' | 'external';++ capabilities: string[]; capacity?: Record<string, any>; latency_ms?: Record<string, number>; health: { status: 'up' | 'degraded' | 'down' | 'draining' };++ pricing: { model: string; marginal_usd_per_unit?: number; rate_card?: string } };++export type Requirements = { kind: string; mobility: 'request' | 'job' | 'session' | 'stateful-partition'; latency_class: 'realtime' | 'interactive' | 'background' | 'bulk' | 'critical';++ objective: 'cheapest' | 'lowest-latency' | 'balanced' | 'private' | 'local-only' | 'first-party-only' | 'high-reliability' | 'correctness';++ max_latency_ms?: number; max_cost_usd?: number; residency?: string; region?: string; trust?: string[]; capabilities?: string[];++ resources?: { cpu?: number; memory_mb?: number; gpu?: boolean }; units?: number; authority?: string; latency_op?: string };++export type RateCard = { id: string; provider: string; metric: string; unit_size: number; unit_price_usd: number; free_allowance: number; reset_period: string };++export type ProviderState = { provider: string; period_start: string; period_end: string; usage: Record<string, number>; forecast?: Record<string, number>; reserve?: Record<string, number> };++export type Candidate = { id: string; eligible: boolean; excluded_because?: string; estimated_cost_usd?: number; estimated_latency_ms?: number; score?: number };++export type PlacementResult = { selected: string | null; objective: string; reasons: string[]; candidates: Candidate[]; decided_at: string };++export declare function plan(req: Requirements, offers: Offer[], opts?: { rateCards?: RateCard[] | Map<string, RateCard>; states?: ProviderState[] | Map<string, ProviderState>;++ now?: number; current?: string | null; minGain?: number; weights?: { cost: number; latency: number }; owned?: { busy?: number; full?: number; maxUsd?: number } }): PlacementResult;++export declare function excluded(req: Requirements, offer: Offer): string | null;++export declare function marginalCost(card: RateCard, state: ProviderState | undefined, projected: number, opts?: { now?: number; priority?: 'normal' | 'high' }): number;++export declare function priceOf(card: RateCard, units: number): number;++export declare function forecast(used: number, start: number, end: number, now: number): number;++export declare function ownedLoadCost(offer: Offer, opts?: { busy?: number; full?: number; maxUsd?: number }): number;++export declare function rendezvous<T extends { id: string; weight?: number }>(key: string, targets: T[]): T | null;++export declare function pickTwo<T>(items: T[], load: (x: T) => number, rand?: () => number): T | null;++export type RoutePlan = { epoch: number; issued_at: string; expires_at: string; issuer: string; key_id: string; routes: { route: string; targets: { id: string; weight: number }[]; draining?: string[]; since_epoch?: number }[]; signature?: string };++export declare function signPlan(plan: RoutePlan, privateKey: any): RoutePlan;++export declare function verifyPlan(plan: RoutePlan, publicKeys: Record<string, any>, opts?: { now?: number }): { ok: boolean; reason?: string };++export declare function createPlanHolder(publicKeys: Record<string, any>, opts?: { now?: () => number; onReject?: (why: string) => void }): { get(): RoutePlan | null; accept(p: RoutePlan): boolean; targets(route: string): { id: string; weight: number }[] | null };++export declare function canonical(v: unknown): string;
@@ -8,6 +8,11 @@ release may change an API and says so here.
@@ -8,6 +8,11 @@ release may change an API and says so here.
+## 0.22.0 (2026-09-28)
+
+- **`openvibe-sdk/placement`** (roadmap WS-Z9, the adaptive fabric): `plan(requirements, offers, { rateCards, states, current })` places a workload on the best eligible node or provider and explains it (platform.placement-result@1). Hard constraints first (capabilities, trust, residency, health, latency ceiling, capacity, cost ceiling; excluded candidates carry the reason), then the objective (cheapest, lowest-latency, balanced; private and first-party-only never leave first-party capacity; critical work stays with its authority). `marginalCost` prices work in a provider's real billing unit after its free allowance, forecast to the end of the period, with a reserve kept for high-priority work; owned capacity is ~free until its binding resource is busy, then work spills over. Hysteresis keeps a placement unless an alternative is clearly better (15% by default); failover is immediate. `rendezvous` (weighted highest-random-weight hashing), `pickTwo` (power of two choices), and Ed25519-signed route plans: `signPlan`, `verifyPlan`, `createPlanHolder` (keeps the last valid plan when a new one is bad, expired or older).
+- **`openvibe-sdk/govern`** (roadmap WS-Z1): weighted cost units (common.resource-cost@1) with quotas per subject and tier over minute, hour, day and month windows; `reserve` → `commit(actual)` or `release`, settled by any process; idempotency keys so a retried charge counts once; concurrency leases. Valkey holds the counters (atomic Lua); without Valkey they live in the process.
+
## 0.21.3 (2026-09-28)
## 0.21.3 (2026-09-28)
- `openvibe-sdk/testing` `createTestDb`: on the containers it also returns `url` and `directUrl`, the `DATABASE_URL` and `DATABASE_DIRECT_URL` of its schema, for a test that spawns a process of the service (OpenRe's transport workers); `null` on PGlite.
- `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.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.21.3",
+ "version": "0.22.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.21.3, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.22.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);
@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.21.3';
+const SDK_VERSION = '0.22.0';
module.exports = {
module.exports = {
createClient,
createClient,
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/govern.js. Do not edit.
+import cjs from '../src/govern.js';
+
+export const {
+ createGovernor, WINDOWS, UNITS,
+} = cjs;
+export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+// Generated by scripts/esm.js from src/placement.js. Do not edit.
+import cjs from '../src/placement.js';
+
+export const {
+ plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost,
+ rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical,
+} = cjs;
+export default cjs;
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.21.3",
+ "version": "0.22.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.21.3",
+ "version": "0.22.0",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.21.3",
+ "version": "0.22.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -76,6 +76,18 @@
@@ -76,6 +76,18 @@
"import": "./esm/limits.mjs",
"import": "./esm/limits.mjs",
"default": "./src/limits.js"
"default": "./src/limits.js"
},
},
+ "./govern": {
+ "types": "./types/govern.d.ts",
+ "browser": null,
+ "import": "./esm/govern.mjs",
+ "default": "./src/govern.js"
+ },
+ "./placement": {
+ "types": "./types/placement.d.ts",
+ "browser": null,
+ "import": "./esm/placement.mjs",
+ "default": "./src/placement.js"
+ },
"./db": {
"./db": {
"types": "./types/db.d.ts",
"types": "./types/db.d.ts",
"browser": null,
"browser": null,
@@ -25,6 +25,8 @@ const ENTRIES = {
@@ -25,6 +25,8 @@ const ENTRIES = {
'community.mjs': '../src/community.js',
'community.mjs': '../src/community.js',
'search.mjs': '../src/search.js',
'search.mjs': '../src/search.js',
'limits.mjs': '../src/limits.js',
'limits.mjs': '../src/limits.js',
+ 'govern.mjs': '../src/govern.js',
+ 'placement.mjs': '../src/placement.js',
'db.mjs': '../src/db/index.js',
'db.mjs': '../src/db/index.js',
'valkey.mjs': '../src/valkey.js',
'valkey.mjs': '../src/valkey.js',
'cache.mjs': '../src/cache.js',
'cache.mjs': '../src/cache.js',
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.21.3';
+const SDK_VERSION = '0.22.0';
module.exports = {
module.exports = {
createClient,
createClient,
new file mode 100644
new file mode 100644
@@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
+'use strict';
+/**
+ * openvibe-sdk/govern — one resource-governance layer for every product (roadmap WS-Z1, decision 40): weighted cost
+ * units (common.resource-cost@1), quotas per subject and tier over several windows, reserve → commit | release so
+ * a failed job gives its cost back, idempotency keys so a retried charge counts once, and concurrency leases.
+ * Valkey holds the counters across processes and hosts; without Valkey they live in this process (tests, one
+ * process).
+ *
+ * const gov = createGovernor({ policy, valkey, now });
+ * const r = await gov.reserve({ subject: 'user:usr_…', tier: 'user', unit: 'browser-second', amount: 30, key: 'run_1:step_2' });
+ * if (!r.ok) → 429 with r.retryAfterS (r.window, r.limit, r.used)
+ * await gov.commit(r.id, 12.5); // the real amount; the difference goes back
+ * await gov.release(r.id); // the work never happened
+ * const lease = await gov.lease({ subject, kind: 'browser', max: 2, ttlMs: 60000 }); // { ok, id, release() }
+ *
+ * policy: { [unit]: { [tier]: { minute?, hour?, day?, month? } } } — a missing tier or window is unlimited, 0
+ * refuses. Contracts publishes the numbers so pages and the API read the same ones (WS-Z1 task 5).
+ */
+const crypto = require('crypto');
+
+const WINDOWS = { minute: 60e3, hour: 3600e3, day: 86400e3, month: 30 * 86400e3 };
+const UNITS = ['browser-second', 'ai-token', 'ai-usd', 'gpu-second', 'video-minute', 'bandwidth-byte', 'storage-byte-day', 'event',
+ 'watch-check', 'bot-control-second', 'job-run', 'upload-byte', 'download-byte'];
+
+function windowsFor(policy, unit, tier, t) {
+ const p = (policy[unit] && policy[unit][tier]) || null;
+ if (!p) return [];
+ return Object.keys(WINDOWS).filter((w) => p[w] != null).map((w) => {
+ const len = WINDOWS[w]; const start = Math.floor(t / len) * len;
+ return { w, limit: Number(p[w]), start, len, ttlMs: start + len - t };
+ });
+}
+
+// ── Stores ───────────────────────────────────────────────────────────────────
+
+function memoryStore() {
+ const counters = new Map(); const reservations = new Map(); const leases = new Map();
+ const sweep = (t) => { for (const [k, v] of counters) if (v.until <= t) counters.delete(k); for (const [k, v] of reservations) if (v.until <= t) reservations.delete(k); };
+ return {
+ async reserve(id, keys, amount, ttlMs, t) {
+ sweep(t);
+ if (reservations.has(id)) return { ok: true, replay: true, reservation: reservations.get(id) };
+ for (const k of keys) {
+ const c = counters.get(k.key);
+ if ((c ? c.used : 0) + amount > k.limit) return { ok: false, window: k.w, limit: k.limit, used: c ? c.used : 0, retryMs: k.ttlMs };
+ }
+ for (const k of keys) {
+ const c = counters.get(k.key) || { used: 0, until: t + k.ttlMs };
+ c.used += amount; counters.set(k.key, c);
+ }
+ const r = { amount, keys: keys.map((k) => k.key), until: t + ttlMs };
+ reservations.set(id, r);
+ return { ok: true, reservation: r };
+ },
+ async settle(id, actual, t) {
+ sweep(t);
+ const r = reservations.get(id);
+ if (!r) return false;
+ const delta = actual == null ? -r.amount : actual - r.amount;
+ for (const key of r.keys) { const c = counters.get(key); if (c) c.used = Math.max(0, c.used + delta); }
+ reservations.delete(id);
+ return true;
+ },
+ async used(key, t) { sweep(t); const c = counters.get(key); return c ? c.used : 0; },
+ async lease(key, id, max, ttlMs, t) {
+ const m = leases.get(key) || new Map();
+ for (const [lid, until] of m) if (until <= t) m.delete(lid);
+ if (m.size >= max) return false;
+ m.set(id, t + ttlMs); leases.set(key, m);
+ return true;
+ },
+ async unlease(key, id) { const m = leases.get(key); if (m) m.delete(id); },
+ };
+}
+
+const RESERVE = `
+-- KEYS: reservation key, then one counter key per window. ARGV: amount, reservation ttl ms, then limit, ttl ms per window.
+if redis.call('EXISTS', KEYS[1]) == 1 then return {1, 1} end
+local amount = tonumber(ARGV[1])
+for i = 2, #KEYS do
+ local used = tonumber(redis.call('GET', KEYS[i]) or '0')
+ local limit = tonumber(ARGV[3 + (i - 2) * 2])
+ if used + amount > limit then return {0, i - 1, tostring(used), ARGV[4 + (i - 2) * 2]} end
+end
+for i = 2, #KEYS do
+ redis.call('INCRBYFLOAT', KEYS[i], amount)
+ redis.call('PEXPIRE', KEYS[i], tonumber(ARGV[4 + (i - 2) * 2]) + 5000)
+end
+local keys = {}
+for i = 2, #KEYS do keys[#keys + 1] = KEYS[i] end
+redis.call('SET', KEYS[1], cjson.encode({ amount = amount, keys = keys }), 'PX', tonumber(ARGV[2]))
+return {1, 0}`;
+const SETTLE = `
+-- KEYS: reservation key. ARGV: the actual amount, or '' to release it all. Moves each counter the reservation
+-- charged by (actual - reserved), never below 0, and ends the reservation. Any process may settle it.
+local raw = redis.call('GET', KEYS[1])
+if not raw then return 0 end
+local r = cjson.decode(raw)
+local delta = -r.amount
+if ARGV[1] ~= '' then delta = tonumber(ARGV[1]) - r.amount end
+redis.call('DEL', KEYS[1])
+for _, k in ipairs(r.keys) do
+ local v = tonumber(redis.call('INCRBYFLOAT', k, delta))
+ if v < 0 then redis.call('SET', k, '0', 'KEEPTTL') end
+end
+return 1`;
+const LEASE = `
+-- KEYS: lease set. ARGV: id, max, ttl ms, now ms.
+redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[4])
+if redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[2]) then return 0 end
+redis.call('ZADD', KEYS[1], tonumber(ARGV[4]) + tonumber(ARGV[3]), ARGV[1])
+redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[3]) + 5000)
+return 1`;
+
+function valkeyStore(valkey) {
+ const c = valkey.client;
+ if (typeof c.ovGovReserve !== 'function') {
+ c.defineCommand('ovGovReserve', { lua: RESERVE });
+ c.defineCommand('ovGovSettle', { numberOfKeys: 1, lua: SETTLE });
+ c.defineCommand('ovGovLease', { numberOfKeys: 1, lua: LEASE });
+ }
+ const rkey = (id) => valkey.key('gov', 'res', id);
+ return {
+ async reserve(id, keys, amount, ttlMs) {
+ const args = [String(amount), String(ttlMs)];
+ for (const k of keys) args.push(String(k.limit), String(Math.max(1, Math.ceil(k.ttlMs))));
+ const r = await c.ovGovReserve(1 + keys.length, rkey(id), ...keys.map((k) => valkey.key('gov', k.key)), ...args);
+ if (Number(r[0]) === 1) return { ok: true, replay: Number(r[1]) === 1 };
+ const k = keys[Number(r[1]) - 1];
+ return { ok: false, window: k.w, limit: k.limit, used: Number(r[2]), retryMs: Number(r[3]) };
+ },
+ async settle(id, actual) { return Number(await c.ovGovSettle(rkey(id), actual == null ? '' : String(actual))) === 1; },
+ async used(key) { return Number((await c.get(valkey.key('gov', key))) || 0); },
+ async lease(key, id, max, ttlMs, t) { return Number(await c.ovGovLease(valkey.key('gov', 'lease', key), id, String(max), String(ttlMs), String(t))) === 1; },
+ async unlease(key, id) { await c.zrem(valkey.key('gov', 'lease', key), id); },
+ };
+}
+
+// ── The governor ─────────────────────────────────────────────────────────────
+
+function createGovernor({ policy = {}, valkey = null, now = () => Date.now(), reservationTtlMs = 6 * 3600e3, onRefused = null, onUsage = null } = {}) {
+ const store = valkey ? valkeyStore(valkey) : memoryStore();
+
+ async function reserve({ subject, tier = 'user', unit, amount, key, project = null }) {
+ if (!UNITS.includes(unit)) throw new TypeError(`govern: unknown unit ${unit}`);
+ if (!(amount >= 0)) throw new TypeError('govern: amount must be ≥ 0');
+ if (!key || String(key).length < 8) throw new TypeError('govern: an idempotency key of 8+ characters is required');
+ const t = now();
+ const id = crypto.createHash('sha256').update(`${subject}\u0000${unit}\u0000${key}`).digest('base64url').slice(0, 32);
+ const scope = project ? `${project}:${subject}` : subject;
+ const keys = windowsFor(policy, unit, tier, t).map((w) => ({ ...w, key: `${unit}:${w.w}:${w.start}:${scope}` }));
+ const r = await store.reserve(id, keys, Number(amount), reservationTtlMs, t);
+ if (!r.ok) {
+ const out = { ok: false, unit, window: r.window, limit: r.limit, used: r.used, retryAfterS: Math.max(1, Math.ceil(r.retryMs / 1000)) };
+ if (onRefused) onRefused({ subject, tier, unit, amount, ...out });
+ return out;
+ }
+ if (onUsage && !r.replay) onUsage({ subject, project, unit, amount: Number(amount), state: 'reserved', idempotency_key: key, at: new Date(t).toISOString() });
+ return { ok: true, id, replay: Boolean(r.replay) };
+ }
+
+ /** The real amount used: counters move by (actual − reserved); the reservation ends. False when it had already ended. */
+ async function commit(id, actual) {
+ if (!(actual >= 0)) throw new TypeError('govern: commit needs the actual amount (≥ 0)');
+ return await store.settle(id, Number(actual), now());
+ }
+
+ /** The work never happened: everything reserved goes back. */
+ async function release(id) {
+ return await store.settle(id, null, now());
+ }
+
+ /** Used so far in each window of a unit, for a subject and tier (for dashboards and the limits page). */
+ async function usage({ subject, tier = 'user', unit, project = null }) {
+ const t = now(); const scope = project ? `${project}:${subject}` : subject;
+ const out = {};
+ for (const w of windowsFor(policy, unit, tier, t)) out[w.w] = { used: await store.used(`${unit}:${w.w}:${w.start}:${scope}`, t), limit: w.limit };
+ return out;
+ }
+
+ /** At most `max` at once for (subject, kind); a lease expires by itself after ttlMs. */
+ async function lease({ subject, kind, max, ttlMs = 60e3 }) {
+ const id = crypto.randomBytes(9).toString('base64url');
+ const key = `${kind}:${subject}`;
+ const ok = await store.lease(key, id, max, ttlMs, now());
+ return ok ? { ok: true, id, release: () => store.unlease(key, id) } : { ok: false, retryAfterS: Math.max(1, Math.ceil(ttlMs / 1000)) };
+ }
+
+ return { reserve, commit, release, usage, lease, policy };
+}
+
+module.exports = { createGovernor, WINDOWS, UNITS };
new file mode 100644
new file mode 100644
@@ -0,0 +1,257 @@
@@ -0,0 +1,257 @@
+'use strict';
+/**
+ * openvibe-sdk/placement — where a workload runs (roadmap WS-Z9, decision 43; the event fabric WS-Z3 and the provider
+ * router WS-Z10 use the same planner). Pure functions over data, no I/O: callers pass the offers they know
+ * (platform.resource-offer@1), the provider rate cards and usage (platform.rate-card@1, platform.provider-state@1)
+ * and the workload's requirements (platform.workload-requirements@1).
+ *
+ * const { plan, marginalCost, rendezvous, pickTwo, signPlan, verifyPlan } = require('openvibe-sdk/placement');
+ * const r = plan(requirements, offers, { rateCards, states, now, current }); // platform.placement-result@1
+ *
+ * The rules:
+ * - hard constraints first: capabilities, trust, residency/region, health, latency ceiling, capacity, cost ceiling.
+ * A cheaper candidate that misses one is excluded, with the reason, never chosen;
+ * - then the objective: `lowest-latency` minimises measured latency within the budget; `cheapest` minimises
+ * marginal cost within the latency ceiling; `balanced` weighs both; `correctness` keeps the authority (the
+ * candidate named in `requirements.authority`, or the current placement) and never moves for cost;
+ * - marginal cost is priced in each provider's real billing unit after its free allowance, forecast to the end of
+ * the period and with a reserve kept for higher-priority work; owned (prepaid) capacity costs ~0 until its
+ * binding resource is busy, then rises so work spills to free or paid capacity before the node saturates;
+ * - hysteresis: a candidate replaces the current placement only when it is clearly better (`minGain`), or when
+ * the current one is no longer eligible (failover is immediate).
+ */
+const crypto = require('crypto');
+
+const TRUST_ORDER = ['first-party', 'partner', 'community', 'external'];
+const HEALTH_OK = new Set(['up', 'degraded']);
+
+// ── Cost ─────────────────────────────────────────────────────────────────────
+
+/** Price of `units` in one rate card over a period, after its free allowance: whole billing units (unit_size). */
+function priceOf(card, units) {
+ const billable = Math.max(0, units - (Number(card.free_allowance) || 0));
+ if (!billable) return 0;
+ const size = Number(card.unit_size) || 1;
+ return (billable / size) * (Number(card.unit_price_usd) || 0); // unit_price_usd is per unit_size, prorated as providers bill
+}
+
+/**
+ * Linear forecast of a period's usage: what was used so far, run to the end of the period at the same rate.
+ * `start`/`end`/`now` in ms.
+ */
+function forecast(used, start, end, now) {
+ const elapsed = Math.max(1, now - start);
+ const total = Math.max(elapsed, end - start);
+ return used * (total / elapsed);
+}
+
+/**
+ * The marginal cost of `projected` more units on a provider this period: cost(forecast + projected) −
+ * cost(forecast), where the free allowance shrinks by the reserve kept for higher-priority work (unless this work
+ * is itself high priority). When the forecast says the allowance runs out anyway, the next unit is priced as paid:
+ * a free allowance that will not last is not free.
+ */
+function marginalCost(card, state, projected, { now = Date.now(), priority = 'normal' } = {}) {
+ const metric = card.metric;
+ const used = Number((state && state.usage && state.usage[metric]) || 0);
+ const start = state && state.period_start ? Date.parse(state.period_start) : now;
+ const end = state && state.period_end ? Date.parse(state.period_end) : now;
+ const expected = state && state.forecast && state.forecast[metric] != null ? Number(state.forecast[metric]) : forecast(used, start, end, now);
+ const reserveShare = priority === 'high' ? 0 : Number((state && state.reserve && state.reserve[metric]) || 0);
+ const effective = { ...card, free_allowance: (Number(card.free_allowance) || 0) * (1 - reserveShare) };
+ return Math.max(0, priceOf(effective, expected + projected) - priceOf(effective, expected));
+}
+
+/**
+ * Owned (prepaid) capacity: ~0 while the binding resource is below `busy` (default 0.6), rising steeply to `max`
+ * above it, so background work spills elsewhere before the node saturates. Utilisation is the highest of CPU,
+ * memory, disk and GPU the offer reports.
+ */
+function ownedLoadCost(offer, { busy = 0.6, full = 0.9, maxUsd = 0.001 } = {}) {
+ const c = offer.capacity || {};
+ const u = Math.max(0, ...['cpu', 'memory', 'disk', 'gpu'].map((k) => Number((c[k] && c[k].utilization) || 0)));
+ if (u <= busy) return 0;
+ if (u >= full) return maxUsd;
+ return maxUsd * ((u - busy) / (full - busy)) ** 2;
+}
+
+// ── Eligibility ──────────────────────────────────────────────────────────────
+
+function latencyOf(offer, op) {
+ const l = offer.latency_ms || {};
+ if (op && l[op] != null) return Number(l[op]);
+ const vals = Object.entries(l).filter(([k]) => /p95/.test(k)).map(([, v]) => Number(v));
+ return vals.length ? Math.min(...vals) : null;
+}
+
+/** Why an offer cannot run the workload, or null when it can. */
+function excluded(req, offer) {
+ const caps = new Set(offer.capabilities || []);
+ for (const c of req.capabilities || []) if (!caps.has(c)) return `lacks ${c}`;
+ const trust = req.trust && req.trust.length ? req.trust : TRUST_ORDER;
+ if (!trust.includes(offer.trust)) return `workload requires ${trust.join(' or ')} trust`;
+ if (req.residency && offer.region !== req.residency && !(offer.regions || []).includes(req.residency)) return `outside ${req.residency}`;
+ if (req.region && req.region !== 'nearest' && offer.region !== req.region && offer.region !== 'global') return `not in ${req.region}`;
+ const h = offer.health || {};
+ if (!HEALTH_OK.has(h.status)) return `health ${h.status || 'unknown'}`;
+ const lat = latencyOf(offer, req.latency_op);
+ if (req.max_latency_ms != null && lat != null && lat > req.max_latency_ms) return `p95 ${lat} ms over ${req.max_latency_ms} ms`;
+ const r = req.resources || {};
+ const c = offer.capacity || {};
+ if (r.cpu && c.cpu && c.cpu.available_cores != null && c.cpu.available_cores < r.cpu) return `${c.cpu.available_cores} cores free, needs ${r.cpu}`;
+ if (r.memory_mb && c.memory && c.memory.available_mb != null && c.memory.available_mb < r.memory_mb) return `${c.memory.available_mb} MB free, needs ${r.memory_mb}`;
+ if (r.gpu && !(c.gpu && c.gpu.type)) return 'no GPU';
+ for (const cap of req.capabilities || []) {
+ const kind = /^worker:(.+)$/.exec(cap);
+ if (kind && c.workers && c.workers[kind[1]] === 0) return `no free ${kind[1]} worker`;
+ }
+ return null;
+}
+
+// ── The planner ──────────────────────────────────────────────────────────────
+
+function estimateCost(req, offer, ctx) {
+ const units = Number(req.units || 1);
+ if (offer.pricing && offer.pricing.model === 'prepaid') return ownedLoadCost(offer, ctx.owned) * units;
+ const card = offer.pricing && offer.pricing.rate_card && ctx.rateCards.get(offer.pricing.rate_card);
+ if (card) return marginalCost(card, ctx.states.get(card.provider) || ctx.states.get(offer.provider), units, { now: ctx.now, priority: req.latency_class === 'critical' || req.latency_class === 'realtime' ? 'high' : 'normal' });
+ const per = offer.pricing && offer.pricing.marginal_usd_per_unit;
+ return per != null ? Number(per) * units : Infinity; // an unpriced paid provider is never assumed free
+}
+
+/**
+ * plan(requirements, offers, { rateCards, states, now, current, minGain, weights }) → platform.placement-result@1.
+ * rateCards: array or Map by id; states: array or Map by provider (platform.provider-state@1); current: the id
+ * the workload runs on now (hysteresis); minGain: relative improvement needed to move (default 0.15).
+ */
+function plan(req, offers, { rateCards = [], states = [], now = Date.now(), current = null, minGain = 0.15, weights = { cost: 0.5, latency: 0.5 }, owned = {} } = {}) {
+ const ctx = {
+ now, owned,
+ rateCards: rateCards instanceof Map ? rateCards : new Map(rateCards.map((c) => [c.id, c])),
+ states: states instanceof Map ? states : new Map(states.map((s) => [s.provider, s])),
+ };
+ const objective = req.objective || 'balanced';
+ // Objectives that are really constraints: private and first-party work never leaves first-party capacity.
+ if ((objective === 'first-party-only' || objective === 'private') && !(req.trust && req.trust.length)) req = { ...req, trust: ['first-party'] };
+ const candidates = offers.map((o) => {
+ const why = excluded(req, o);
+ if (why) return { id: o.offer_id, eligible: false, excluded_because: why };
+ const cost = estimateCost(req, o, ctx);
+ if (req.max_cost_usd != null && cost > req.max_cost_usd) return { id: o.offer_id, eligible: false, excluded_because: `estimated $${cost.toFixed(6)} over $${req.max_cost_usd}` };
+ const lat = latencyOf(o, req.latency_op);
+ return { id: o.offer_id, eligible: true, estimated_cost_usd: cost, estimated_latency_ms: lat == null ? undefined : lat, trust: o.trust };
+ });
+ const ok = candidates.filter((c) => c.eligible && Number.isFinite(c.estimated_cost_usd));
+ const reasons = [];
+ if (!ok.length) return { selected: null, objective, reasons: ['no eligible candidate'], candidates: strip(candidates), decided_at: new Date(now).toISOString() };
+
+ const maxCost = Math.max(...ok.map((c) => c.estimated_cost_usd), 1e-12);
+ const lats = ok.map((c) => c.estimated_latency_ms).filter((x) => x != null);
+ const maxLat = Math.max(...lats, 1);
+ for (const c of ok) {
+ const nc = c.estimated_cost_usd / maxCost;
+ const nl = (c.estimated_latency_ms == null ? maxLat : c.estimated_latency_ms) / maxLat;
+ c.score = objective === 'cheapest' ? nc + nl * 1e-3
+ : objective === 'lowest-latency' ? nl + nc * 1e-3
+ : nc * weights.cost + nl * weights.latency;
+ c.score = Number(c.score.toFixed(6));
+ }
+ ok.sort((a, b) => a.score - b.score || (a.estimated_latency_ms ?? Infinity) - (b.estimated_latency_ms ?? Infinity));
+ let best = ok[0];
+ const cur = current && ok.find((c) => c.id === current);
+ if (objective === 'correctness' || req.latency_class === 'critical') {
+ const authority = (req.authority && ok.find((c) => c.id === req.authority)) || cur;
+ if (authority) { best = authority; reasons.push('the authority keeps critical work'); }
+ } else if (cur && cur !== best) {
+ const gain = cur.score > 0 ? (cur.score - best.score) / cur.score : 0;
+ if (gain < minGain) { best = cur; reasons.push(`stays on ${cur.id}: the best alternative is only ${(gain * 100).toFixed(1)}% better (needs ${(minGain * 100).toFixed(0)}%)`); }
+ } else if (current && !cur) reasons.push(`${current} is no longer eligible: moved`);
+ if (best.estimated_cost_usd === 0) reasons.push('no marginal cost (already-paid capacity or a free allowance)');
+ if (req.max_latency_ms != null) reasons.push(`meets the ${req.max_latency_ms} ms latency ceiling`);
+ reasons.push(`objective ${objective}`);
+ return { selected: best.id, objective, reasons, candidates: strip(candidates), decided_at: new Date(now).toISOString() };
+}
+
+function strip(cands) {
+ return cands.map(({ trust, ...c }) => c); // platform.placement-result@1 candidate shape
+}
+
+// ── Stable assignment and local balancing ────────────────────────────────────
+
+/** Weighted rendezvous (highest random weight) hashing: the target for `key`; adding or removing one moves little. */
+function rendezvous(key, targets) {
+ let best = null; let bestScore = -Infinity;
+ for (const t of targets) {
+ const w = Number(t.weight == null ? 1 : t.weight);
+ if (!(w > 0)) continue;
+ const h = crypto.createHash('sha256').update(`${t.id}\u0000${key}`).digest();
+ const u = (h.readUInt32BE(0) + 1) / 4294967297; // (0, 1)
+ const score = -w / Math.log(u);
+ if (score > bestScore) { bestScore = score; best = t; }
+ }
+ return best;
+}
+
+/** Power of two choices: two random candidates, the one with less load (least-request without a thundering herd). */
+function pickTwo(items, load, rand = Math.random) {
+ if (!items.length) return null;
+ if (items.length === 1) return items[0];
+ const i = Math.floor(rand() * items.length);
+ let j = Math.floor(rand() * (items.length - 1));
+ if (j >= i) j++;
+ return load(items[j]) < load(items[i]) ? items[j] : items[i];
+}
+
+// ── Signed route plans (platform.placement-plan@1) ───────────────────────────
+
+function canonical(v) {
+ if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
+ if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;
+ return JSON.stringify(v);
+}
+
+/** Sign a plan with an Ed25519 private key (PEM or KeyObject): returns the plan with `signature`. */
+function signPlan(planBody, privateKey) {
+ const { signature, ...body } = planBody;
+ const sig = crypto.sign(null, Buffer.from(canonical(body)), privateKey);
+ return { ...body, signature: sig.toString('base64url') };
+}
+
+/**
+ * Verify a plan: its key id is known, the signature matches, and it has not expired. Returns { ok, reason }.
+ * publicKeys: { [key_id]: PEM | KeyObject }. A caller keeps using its last valid plan when a new one fails this.
+ */
+function verifyPlan(p, publicKeys, { now = Date.now() } = {}) {
+ if (!p || !p.signature) return { ok: false, reason: 'unsigned' };
+ const key = publicKeys && publicKeys[p.key_id];
+ if (!key) return { ok: false, reason: `unknown key ${p.key_id}` };
+ const { signature, ...body } = p;
+ let good = false;
+ try { good = crypto.verify(null, Buffer.from(canonical(body)), key, Buffer.from(signature, 'base64url')); } catch { good = false; }
+ if (!good) return { ok: false, reason: 'bad signature' };
+ if (Date.parse(p.expires_at) <= now) return { ok: false, reason: 'expired' };
+ return { ok: true };
+}
+
+/**
+ * A holder for the current route plan: accept() takes a newer valid plan and keeps the old one otherwise, so a
+ * control-plane outage or a bad plan never stops delivery (decision 42).
+ */
+function createPlanHolder(publicKeys, { now = () => Date.now(), onReject = null } = {}) {
+ let current = null;
+ return {
+ get: () => current,
+ accept(p) {
+ const v = verifyPlan(p, publicKeys, { now: now() });
+ if (!v.ok || (current && p.epoch <= current.epoch)) {
+ if (onReject) onReject(v.ok ? `epoch ${p.epoch} is not newer than ${current.epoch}` : v.reason);
+ return false;
+ }
+ current = p;
+ return true;
+ },
+ targets(route) { const r = current && current.routes.find((x) => x.route === route); return r ? r.targets : null; },
+ };
+}
+
+module.exports = { plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical };
@@ -76,7 +76,7 @@ run([
@@ -76,7 +76,7 @@ run([
for (const [re, why] of FORBIDDEN) if (re.test(src)) problems.push(`${path.relative(ROOT, file)}: ${why}`);
for (const [re, why] of FORBIDDEN) if (re.test(src)) problems.push(`${path.relative(ROOT, file)}: ${why}`);
}
}
assert.deepEqual(problems, []);
assert.deepEqual(problems, []);
- assert.deepEqual(blocked.sort(), ['./cache', './db', './events', './identity', './limits', './openre', './pubsub', './queue', './testing', './valkey', './vip']);
+ assert.deepEqual(blocked.sort(), ['./cache', './db', './events', './govern', './identity', './limits', './openre', './placement', './pubsub', './queue', './testing', './valkey', './vip']);
const rel = [...scanned].map((f) => path.relative(ROOT, f));
const rel = [...scanned].map((f) => path.relative(ROOT, f));
for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
assert.ok(rel.includes(must), `${must} is scanned`);
assert.ok(rel.includes(must), `${must} is scanned`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+'use strict';
+/** openvibe-sdk/govern: quotas by tier and window, reserve/commit/release, idempotent charges, concurrency leases; in memory and on Valkey. */
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const { run } = require('./helpers');
+const { createGovernor } = require('../src/govern');
+const { createValkey } = require('../src/valkey');
+
+const policy = { 'browser-second': { anonymous: { day: 0 }, user: { minute: 100, day: 600 } }, 'job-run': { user: { hour: 3 } } };
+const quiet = { log() {}, warn() {}, error() {} };
+
+function suite(label, make) {
+ return [
+ [`${label}: a reservation counts against every window; over the limit is refused with a retry time`, async () => {
+ const gov = await make();
+ const a = await gov.reserve({ subject: 'user:u1', unit: 'browser-second', amount: 60, key: 'run-1:step-1' });
+ assert.equal(a.ok, true);
+ const b = await gov.reserve({ subject: 'user:u1', unit: 'browser-second', amount: 50, key: 'run-1:step-2' });
+ assert.equal(b.ok, false);
+ assert.equal(b.window, 'minute');
+ assert.equal(b.limit, 100);
+ assert.ok(b.retryAfterS >= 1 && b.retryAfterS <= 60);
+ const u = await gov.usage({ subject: 'user:u1', unit: 'browser-second' });
+ assert.equal(u.minute.used, 60);
+ assert.equal(u.day.limit, 600);
+ }],
+ [`${label}: commit moves the counters to the real amount; release gives everything back`, async () => {
+ const gov = await make();
+ const a = await gov.reserve({ subject: 'user:u2', unit: 'browser-second', amount: 90, key: 'run-2:step-1' });
+ await gov.commit(a.id, 30);
+ assert.equal((await gov.usage({ subject: 'user:u2', unit: 'browser-second' })).minute.used, 30);
+ const b = await gov.reserve({ subject: 'user:u2', unit: 'browser-second', amount: 70, key: 'run-2:step-2' });
+ assert.equal(b.ok, true);
+ await gov.release(b.id);
+ assert.equal((await gov.usage({ subject: 'user:u2', unit: 'browser-second' })).minute.used, 30);
+ assert.equal(await gov.release(b.id), false, 'settling twice changes nothing');
+ }],
+ [`${label}: the same idempotency key charges once`, async () => {
+ const gov = await make();
+ const a = await gov.reserve({ subject: 'user:u3', unit: 'job-run', amount: 1, key: 'job_01J-retry' });
+ const again = await gov.reserve({ subject: 'user:u3', unit: 'job-run', amount: 1, key: 'job_01J-retry' });
+ assert.equal(again.replay, true);
+ assert.equal(again.id, a.id);
+ assert.equal((await gov.usage({ subject: 'user:u3', unit: 'job-run' })).hour.used, 1);
+ }],
+ [`${label}: a zero limit refuses (anonymous browser time); a missing tier is unlimited`, async () => {
+ const gov = await make();
+ assert.equal((await gov.reserve({ subject: 'ip:1', tier: 'anonymous', unit: 'browser-second', amount: 1, key: 'anon-try-1' })).ok, false);
+ assert.equal((await gov.reserve({ subject: 'user:u4', tier: 'vip', unit: 'browser-second', amount: 1e6, key: 'vip-run-1' })).ok, true);
+ await assert.rejects(() => gov.reserve({ subject: 'user:u4', unit: 'coins', amount: 1, key: 'abcdefgh' }), /unknown unit/);
+ await assert.rejects(() => gov.reserve({ subject: 'user:u4', unit: 'job-run', amount: 1, key: 'k' }), /idempotency key/);
+ }],
+ [`${label}: concurrency leases cap parallel work and free a slot on release`, async () => {
+ const gov = await make();
+ const a = await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 });
+ const b = await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 });
+ const c = await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 });
+ assert.deepEqual([a.ok, b.ok, c.ok], [true, true, false]);
+ await a.release();
+ assert.equal((await gov.lease({ subject: 'user:u5', kind: 'browser', max: 2 })).ok, true);
+ }],
+ ];
+}
+
+const tests = suite('memory', async () => createGovernor({ policy, now: () => Date.parse('2026-09-28T12:00:30Z') }));
+const URL = process.env.OV_TEST_VALKEY_URL;
+if (!URL) console.log('govern on valkey: skipped (OV_TEST_VALKEY_URL not set; scripts/test-services.sh up)');
+else {
+ const opened = [];
+ tests.push(...suite('valkey', async () => {
+ const v = createValkey({ url: URL, prefix: `ovsdk:gov:${crypto.randomBytes(3).toString('hex')}:`, log: quiet });
+ opened.push(v);
+ return createGovernor({ policy, valkey: v, now: () => Date.parse('2026-09-28T12:00:30Z') });
+ }));
+ tests.push(['valkey: close', async () => { for (const v of opened) await v.close(); }]);
+}
+run(tests);
@@ -66,7 +66,7 @@ run([
@@ -66,7 +66,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
- assert.equal(subpaths.length, 28);
+ assert.equal(subpaths.length, 30);
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
new file mode 100644
new file mode 100644
@@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
+'use strict';
+/** openvibe-sdk/placement: constraints before cost, marginal cost after free allowances, hysteresis, rendezvous, P2C, signed plans. */
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const { run } = require('./helpers');
+const { plan, marginalCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, ownedLoadCost } = require('../src/placement');
+
+const T = Date.parse('2026-09-16T00:00:00Z'); // half of September gone
+const period = { period_start: '2026-09-01T00:00:00Z', period_end: '2026-10-01T00:00:00Z' };
+const node = (id, util, extra = {}) => ({ offer_id: id, kind: 'node', region: 'us-west', trust: 'first-party', capabilities: ['worker:browser'], capacity: { cpu: { utilization: util, available_cores: 4 }, memory: { available_mb: 8000 } }, latency_ms: { start_p95: 40 }, health: { status: 'up' }, pricing: { model: 'prepaid' }, updated_at: '2026-09-15T00:00:00Z', ...extra });
+const provider = (id, card, extra = {}) => ({ offer_id: id, kind: 'provider', provider: id, region: 'global', trust: 'external', capabilities: ['worker:browser'], latency_ms: { start_p95: 120 }, health: { status: 'up' }, pricing: { model: 'per-operation', rate_card: card }, updated_at: '2026-09-15T00:00:00Z', ...extra });
+const card = (id, provider, free, price, size = 1e6) => ({ id, provider, metric: 'op', unit_size: size, unit_price_usd: price, free_allowance: free, reset_period: 'month', effective_from: '2026-09-01', source: 'https://example.com', verified_at: '2026-09-28' });
+const req = (o = {}) => ({ kind: 'watch.browser', mobility: 'job', latency_class: 'background', objective: 'cheapest', capabilities: ['worker:browser'], ...o });
+
+run([
+ ['a free allowance that will run out before the period ends is priced as paid', async () => {
+ const c = card('rc', 'p', 1e6, 0.4);
+ // Half the month gone, 430k used: the forecast is 860k, so 100k more stay free.
+ assert.equal(marginalCost(c, { provider: 'p', ...period, usage: { op: 430000 } }, 100000, { now: T }), 0);
+ // 600k used by mid-month: forecast 1.2M, past the allowance; the next million is billed.
+ assert.ok(marginalCost(c, { provider: 'p', ...period, usage: { op: 600000 } }, 1e6, { now: T }) > 0);
+ // A 20% reserve for higher-priority work makes normal work pay sooner, never high-priority work.
+ const s = { provider: 'p', ...period, usage: { op: 430000 }, reserve: { op: 0.2 } };
+ assert.ok(marginalCost(c, s, 100000, { now: T }) > 0);
+ assert.equal(marginalCost(c, s, 100000, { now: T, priority: 'high' }), 0);
+ }],
+ ['owned capacity is ~free until its binding resource is busy, then work spills elsewhere', async () => {
+ assert.equal(ownedLoadCost(node('a', 0.3)), 0);
+ assert.ok(ownedLoadCost(node('a', 0.8)) > 0);
+ const cards = [card('rc_q', 'q', 1e6, 0.4)];
+ const states = [{ provider: 'q', ...period, usage: { op: 1000 } }];
+ assert.equal(plan(req(), [node('own', 0.3), provider('q', 'rc_q')], { rateCards: cards, states, now: T }).selected, 'own');
+ assert.equal(plan(req(), [node('own', 0.95), provider('q', 'rc_q')], { rateCards: cards, states, now: T }).selected, 'q');
+ }],
+ ['hard constraints exclude a cheaper candidate, with the reason', async () => {
+ const r = plan(req({ objective: 'balanced', trust: ['first-party'], max_latency_ms: 100 }), [node('own', 0.2), provider('cheap', 'rc'), node('slow', 0.1, { offer_id: 'slow', latency_ms: { start_p95: 400 } })], { rateCards: [card('rc', 'cheap', 1e9, 0)], now: T });
+ assert.equal(r.selected, 'own');
+ assert.match(r.candidates.find((c) => c.id === 'cheap').excluded_because, /first-party/);
+ assert.match(r.candidates.find((c) => c.id === 'slow').excluded_because, /over 100 ms/);
+ assert.equal(plan(req({ objective: 'private' }), [provider('ext', 'rc')], { rateCards: [card('rc', 'ext', 1e9, 0)], now: T }).selected, null, 'private work never leaves first-party capacity');
+ assert.match(plan(req(), [node('down', 0.1, { health: { status: 'down' } })], { now: T }).candidates[0].excluded_because, /health down/);
+ }],
+ ['an unpriced paid provider is never assumed free', async () => {
+ const r = plan(req(), [provider('mystery', 'rc_missing')], { now: T });
+ assert.equal(r.selected, null);
+ }],
+ ['hysteresis: stay unless clearly better; fail over at once when the current one drops out', async () => {
+ const a = node('a', 0.3, { latency_ms: { start_p95: 50 } }); const b = node('b', 0.3, { latency_ms: { start_p95: 46 } });
+ const r = plan(req({ objective: 'lowest-latency' }), [a, b], { current: 'a', now: T });
+ assert.equal(r.selected, 'a');
+ assert.match(r.reasons[0], /only .*% better/);
+ const moved = plan(req({ objective: 'lowest-latency' }), [{ ...a, health: { status: 'down' } }, b], { current: 'a', now: T });
+ assert.equal(moved.selected, 'b');
+ assert.ok(moved.reasons.some((x) => /no longer eligible/.test(x)));
+ }],
+ ['critical work stays with its authority whatever the cost', async () => {
+ const r = plan(req({ latency_class: 'critical', objective: 'correctness', authority: 'ledger' }), [node('ledger', 0.95), node('idle', 0.1)], { now: T });
+ assert.equal(r.selected, 'ledger');
+ }],
+ ['rendezvous hashing is stable and moves little when a target is added', async () => {
+ const t3 = [{ id: 'a', weight: 1 }, { id: 'b', weight: 1 }, { id: 'c', weight: 1 }];
+ const keys = Array.from({ length: 2000 }, (_, i) => `room:${i}`);
+ const before = keys.map((k) => rendezvous(k, t3).id);
+ assert.deepEqual(keys.map((k) => rendezvous(k, t3).id), before);
+ const after = keys.map((k) => rendezvous(k, [...t3, { id: 'd', weight: 1 }]).id);
+ const moved = after.filter((x, i) => x !== before[i]);
+ assert.ok(moved.every((x) => x === 'd'), 'only keys that now belong to the new target move');
+ assert.ok(moved.length > 300 && moved.length < 700, `about a quarter move (${moved.length})`);
+ const heavy = keys.map((k) => rendezvous(k, [{ id: 'a', weight: 3 }, { id: 'b', weight: 1 }]).id).filter((x) => x === 'a').length;
+ assert.ok(heavy > 1300 && heavy < 1700, `weights hold (${heavy}/2000)`);
+ }],
+ ['power of two choices picks the less loaded of two', async () => {
+ const load = { a: 400, b: 120, c: 80 };
+ let r = 0; const seq = [0.1, 0.9]; // picks index 0 and then 2
+ assert.equal(pickTwo(['a', 'b', 'c'], (x) => load[x], () => seq[r++]), 'c');
+ }],
+ ['signed route plans: verified, refused when tampered, expired or older; the last good plan stays', async () => {
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
+ const body = { epoch: 2, issued_at: '2026-09-28T00:00:00Z', expires_at: '2026-09-29T00:00:00Z', issuer: 'network', key_id: 'k1', routes: [{ route: 'events:background', targets: [{ id: 'valkey', weight: 1 }] }] };
+ const p = signPlan(body, privateKey);
+ const now = Date.parse('2026-09-28T12:00:00Z');
+ assert.equal(verifyPlan(p, { k1: publicKey }, { now }).ok, true);
+ assert.equal(verifyPlan({ ...p, routes: [] }, { k1: publicKey }, { now }).reason, 'bad signature');
+ assert.equal(verifyPlan(p, { k1: publicKey }, { now: Date.parse('2026-09-30T00:00:00Z') }).reason, 'expired');
+ assert.equal(verifyPlan(p, { other: publicKey }, { now }).reason, 'unknown key k1');
+ const holder = createPlanHolder({ k1: publicKey }, { now: () => now });
+ assert.equal(holder.accept(p), true);
+ assert.equal(holder.accept(signPlan({ ...body, epoch: 1 }, privateKey)), false, 'an older epoch is ignored');
+ assert.equal(holder.accept({ ...p, epoch: 3 }), false, 'a tampered plan is ignored');
+ assert.deepEqual(holder.targets('events:background'), [{ id: 'valkey', weight: 1 }]);
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+import type { Valkey } from './valkey';
+export type CostUnit = 'browser-second' | 'ai-token' | 'ai-usd' | 'gpu-second' | 'video-minute' | 'bandwidth-byte' | 'storage-byte-day' | 'event'
+ | 'watch-check' | 'bot-control-second' | 'job-run' | 'upload-byte' | 'download-byte';
+export type Windowed = { minute?: number; hour?: number; day?: number; month?: number };
+/** { [unit]: { [tier]: { minute?, hour?, day?, month? } } }: a missing tier or window is unlimited, 0 refuses. */
+export type GovernPolicy = Partial<Record<CostUnit, Record<string, Windowed>>>;
+export type Reserved = { ok: true; id: string; replay: boolean } | { ok: false; unit: CostUnit; window: string; limit: number; used: number; retryAfterS: number };
+export interface Governor {
+ reserve(o: { subject: string; tier?: string; unit: CostUnit; amount: number; key: string; project?: string | null }): Promise<Reserved>;
+ commit(id: string, actual: number): Promise<boolean>;
+ release(id: string): Promise<boolean>;
+ usage(o: { subject: string; tier?: string; unit: CostUnit; project?: string | null }): Promise<Record<string, { used: number; limit: number }>>;
+ lease(o: { subject: string; kind: string; max: number; ttlMs?: number }): Promise<{ ok: true; id: string; release(): Promise<void> } | { ok: false; retryAfterS: number }>;
+ policy: GovernPolicy;
+}
+export declare function createGovernor(opts?: { policy?: GovernPolicy; valkey?: Valkey | null; now?: () => number; reservationTtlMs?: number;
+ onRefused?: (e: object) => void; onUsage?: (e: object) => void }): Governor;
+export declare const WINDOWS: Record<'minute' | 'hour' | 'day' | 'month', number>;
+export declare const UNITS: CostUnit[];
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+/** platform.* contract shapes are validated by openvibe-contracts; these are the fields the planner reads. */
+export type Offer = { offer_id: string; kind: 'node' | 'provider'; provider?: string; region: string; regions?: string[]; trust: 'first-party' | 'partner' | 'community' | 'external';
+ capabilities: string[]; capacity?: Record<string, any>; latency_ms?: Record<string, number>; health: { status: 'up' | 'degraded' | 'down' | 'draining' };
+ pricing: { model: string; marginal_usd_per_unit?: number; rate_card?: string } };
+export type Requirements = { kind: string; mobility: 'request' | 'job' | 'session' | 'stateful-partition'; latency_class: 'realtime' | 'interactive' | 'background' | 'bulk' | 'critical';
+ objective: 'cheapest' | 'lowest-latency' | 'balanced' | 'private' | 'local-only' | 'first-party-only' | 'high-reliability' | 'correctness';
+ max_latency_ms?: number; max_cost_usd?: number; residency?: string; region?: string; trust?: string[]; capabilities?: string[];
+ resources?: { cpu?: number; memory_mb?: number; gpu?: boolean }; units?: number; authority?: string; latency_op?: string };
+export type RateCard = { id: string; provider: string; metric: string; unit_size: number; unit_price_usd: number; free_allowance: number; reset_period: string };
+export type ProviderState = { provider: string; period_start: string; period_end: string; usage: Record<string, number>; forecast?: Record<string, number>; reserve?: Record<string, number> };
+export type Candidate = { id: string; eligible: boolean; excluded_because?: string; estimated_cost_usd?: number; estimated_latency_ms?: number; score?: number };
+export type PlacementResult = { selected: string | null; objective: string; reasons: string[]; candidates: Candidate[]; decided_at: string };
+export declare function plan(req: Requirements, offers: Offer[], opts?: { rateCards?: RateCard[] | Map<string, RateCard>; states?: ProviderState[] | Map<string, ProviderState>;
+ now?: number; current?: string | null; minGain?: number; weights?: { cost: number; latency: number }; owned?: { busy?: number; full?: number; maxUsd?: number } }): PlacementResult;
+export declare function excluded(req: Requirements, offer: Offer): string | null;
+export declare function marginalCost(card: RateCard, state: ProviderState | undefined, projected: number, opts?: { now?: number; priority?: 'normal' | 'high' }): number;
+export declare function priceOf(card: RateCard, units: number): number;
+export declare function forecast(used: number, start: number, end: number, now: number): number;
+export declare function ownedLoadCost(offer: Offer, opts?: { busy?: number; full?: number; maxUsd?: number }): number;
+export declare function rendezvous<T extends { id: string; weight?: number }>(key: string, targets: T[]): T | null;
+export declare function pickTwo<T>(items: T[], load: (x: T) => number, rand?: () => number): T | null;
+export type RoutePlan = { epoch: number; issued_at: string; expires_at: string; issuer: string; key_id: string; routes: { route: string; targets: { id: string; weight: number }[]; draining?: string[]; since_epoch?: number }[]; signature?: string };
+export declare function signPlan(plan: RoutePlan, privateKey: any): RoutePlan;
+export declare function verifyPlan(plan: RoutePlan, publicKeys: Record<string, any>, opts?: { now?: number }): { ok: boolean; reason?: string };
+export declare function createPlanHolder(publicKeys: Record<string, any>, opts?: { now?: () => number; onReject?: (why: string) => void }): { get(): RoutePlan | null; accept(p: RoutePlan): boolean; targets(route: string): { id: string; weight: number }[] | null };
+export declare function canonical(v: unknown): string;