openvibers/openvibe.sdk Commits
Fix new SDK subpath tests and document Fabric exports (#8)
Changes
@@@ -7,8 +7,10 @@ release may change an API and says so here.--## Unreleased++## 0.27.0 (2026-10-03)++- Add server subpaths `openvibe-sdk/cost`, `openvibe-sdk/runtime`, `openvibe-sdk/storage`,++ `openvibe-sdk/delivery`, and `openvibe-sdk/node` for Fabric contract records and offer builders.- **`openvibe-sdk/usage` (new, server; plan T1):** `platform.usage-sample@1` readings in one place.`usageSample(fields)` builds the record — `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit` andevery optional dimension pass through, `at` defaults to now (ISO) and `source` to `openvibe-sdk/usage`, null fields@@@ -183,6 +183,11 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/vip` | server | `createVipClient({baseUrl?, tokenClient \| getToken, fetch?, timeoutMs?})`: `evaluate({subject, resource, owner, fallback, mode})`, `checkEntitlement({subject, creator, product, mode})`, `isMember(subject, creator)`; every failure is a denial. `createVipCache({vip, ttlMs, denyTtlMs, unavailableTtlMs})`: `entitlement`, `peekEntitlement`, `evaluate`, `invalidate`, `handleEvent` (`vip.membership.changed` and Billing's entitlement events drop a member's answers at once), `clear`, `bounds` || `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `openvibe-sdk/govern` builds its `onUsage` record with it || `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute |++| `openvibe-sdk/cost` | server | Build and validate `platform.rate-card@1` and `platform.cost-snapshot@1` records with `rateCard()`, `costSnapshot()`, `validateRateCard()`, and `validateCostSnapshot()`; `estimate()` calculates a charge from a rate card |++| `openvibe-sdk/runtime` | server | Build and validate `platform.runtime-offer@1` records with `runtimeOffer()` and `validateRuntimeOffer()`; `runtimeClass()`, `RUNTIME_CLASSES`, and `validateRuntimeClass()` handle known runtime classes |++| `openvibe-sdk/storage` | server | `storageOffer()` and `validateStorageOffer()` for `platform.storage-offer@1` |++| `openvibe-sdk/delivery` | server | `deliveryOffer()` and `validateDeliveryOffer()` for `platform.delivery-offer@1` |++| `openvibe-sdk/node` | server | `nodeCapabilities()` and `validateNodeCapabilities()` for `platform.node-capabilities@1`; `nodeOffers()` derives resource and runtime offers from a node descriptor || `openvibe-sdk/service` | server | the service kit: `gracefulStop({name, server, stop, close, drainMs, deadlineMs, deadlineExitCode, beforeDrain, handles})` → `{stop, stopping}`, `within(ms, p)`; `createServiceError(name)`, `ServiceError`, `asServiceError`, `run`, `wrap`, `sendError`, `jsonBody({limit})`, `privateNoStore`, `jsonErrors()`; re-exports (loaded on first use) of openvibe-shared `createReadiness`, `skip`, `safeReason`, `createRegistry`, `instrument`, `metricsHandler`, `isLoopbackDirect`, `releaseInfo`, `createRelease` and openvibe-contracts `problem`, `sendProblem` || `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media, Tools jobs and the Tools platform API on an in-process `fetch` || `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, tools, projects |@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.26.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.27.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.26.0';++const SDK_VERSION = '0.27.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/cost.js. Do not edit.++import cjs from '../src/cost.js';++++export const {++ rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/delivery.js. Do not edit.++import cjs from '../src/delivery.js';++++export const {++ deliveryOffer, validateDeliveryOffer,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/node.js. Do not edit.++import cjs from '../src/node.js';++++export const {++ nodeCapabilities, nodeOffers, validateNodeCapabilities,++} = cjs;++export default cjs;@@@ -4,5 +4,6 @@ import cjs from '../src/placement.js';export const {plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost,rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical,++ TRUST_CLASSES,} = cjs;export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/runtime.js. Do not edit.++import cjs from '../src/runtime.js';++++export const {++ runtimeOffer, runtimeClass, RUNTIME_CLASSES, validateRuntimeOffer, validateRuntimeClass,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/storage.js. Do not edit.++import cjs from '../src/storage.js';++++export const {++ storageOffer, validateStorageOffer,++} = cjs;++export default cjs;@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.26.0",++ "version": "0.27.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.26.0",++ "version": "0.27.0","license": "MIT","devDependencies": {"@electric-sql/pglite": "^0.5.8",@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.26.0",++ "version": "0.27.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",@@@ -106,6 +106,36 @@"import": "./esm/telemetry.mjs","default": "./src/telemetry.js"},++ "./cost": {++ "types": "./types/cost.d.ts",++ "browser": null,++ "import": "./esm/cost.mjs",++ "default": "./src/cost.js"++ },++ "./runtime": {++ "types": "./types/runtime.d.ts",++ "browser": null,++ "import": "./esm/runtime.mjs",++ "default": "./src/runtime.js"++ },++ "./storage": {++ "types": "./types/storage.d.ts",++ "browser": null,++ "import": "./esm/storage.mjs",++ "default": "./src/storage.js"++ },++ "./delivery": {++ "types": "./types/delivery.d.ts",++ "browser": null,++ "import": "./esm/delivery.mjs",++ "default": "./src/delivery.js"++ },++ "./node": {++ "types": "./types/node.d.ts",++ "browser": null,++ "import": "./esm/node.mjs",++ "default": "./src/node.js"++ },"./db": {"types": "./types/db.d.ts","browser": null,@@@ -30,6 +30,11 @@ const ENTRIES = {'placement.mjs': '../src/placement.js','usage.mjs': '../src/usage.js','telemetry.mjs': '../src/telemetry.js',++ 'cost.mjs': '../src/cost.js',++ 'runtime.mjs': '../src/runtime.js',++ 'storage.mjs': '../src/storage.js',++ 'delivery.mjs': '../src/delivery.js',++ 'node.mjs': '../src/node.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.26.0';++const SDK_VERSION = '0.27.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,82 @@++'use strict';++/**++ * openvibe-sdk/cost — platform.rate-card@1 and platform.cost-snapshot@1, plus estimate() for a rate card.++ * Pure construction and validation, no I/O and no dependency at require time: openvibe-contracts is required++ * lazily by the validate* functions, the way src/service re-exports its packages.++ *++ * const { rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot } = require('openvibe-sdk/cost');++ * const card = rateCard({ id, provider, metric, unit_size, unit_price_usd, free_allowance, reset_period,++ * effective_from, source, verified_at });++ * estimate(card, 2_500_000, 'ops'); // { quantity, unit, billable, cost_usd }++ */++++/** The fields platform.rate-card@1 requires or allows, in the schema's order; null/undefined are stripped. */++function rateCard(fields) {++ const f = fields || {};++ const record = {++ id: f.id,++ provider: f.provider,++ adapter: f.adapter,++ metric: f.metric,++ unit_size: f.unit_size,++ unit_price_usd: f.unit_price_usd,++ free_allowance: f.free_allowance,++ reset_period: f.reset_period,++ region: f.region,++ effective_from: f.effective_from,++ effective_until: f.effective_until,++ source: f.source,++ verified_at: f.verified_at,++ };++ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];++ return record;++}++++/** The fields platform.cost-snapshot@1 requires or allows; null/undefined are stripped. */++function costSnapshot(fields) {++ const f = fields || {};++ const record = {++ window_start: f.window_start,++ window_end: f.window_end,++ project_id: f.project_id,++ scope: f.scope,++ by_target: f.by_target,++ actual_usd: f.actual_usd,++ counterfactual_usd: f.counterfactual_usd,++ };++ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];++ return record;++}++++/**++ * The cost of `quantity` units in one rate card: billable = max(0, quantity − free_allowance), priced in whole++ * unit_size chunks at unit_price_usd. `unit` is carried through for the caller's label; the math is unit-agnostic.++ */++function estimate(card, quantity, unit) {++ const q = Number(quantity) || 0;++ const free = Number(card.free_allowance) || 0;++ const billable = Math.max(0, q - free);++ const size = Number(card.unit_size) || 1;++ const cost_usd = (billable / size) * (Number(card.unit_price_usd) || 0);++ return { quantity: q, unit: unit || null, billable, cost_usd };++}++++/** { ok, errors } for a record against platform.rate-card@1. */++function validateRateCard(record) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.rate-card@1', record);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++/** { ok, errors } for a record against platform.cost-snapshot@1. */++function validateCostSnapshot(record) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.cost-snapshot@1', record);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++module.exports = { rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot };nnew file mode 100644@@@ -0,0 +1,37 @@++'use strict';++/**++ * openvibe-sdk/delivery — platform.delivery-offer@1.++ * Pure construction and validation, no I/O and no dependency at require time.++ *++ * const { deliveryOffer, validateDeliveryOffer } = require('openvibe-sdk/delivery');++ * const offer = deliveryOffer({ id, transports, regions, edge, price_per_gb_usd, price_per_request_usd,++ * cache_rules });++ */++++/** The fields platform.delivery-offer@1 requires or allows; null/undefined are stripped. */++function deliveryOffer(fields) {++ const f = fields || {};++ const record = {++ id: f.id,++ transports: f.transports,++ regions: f.regions,++ edge: f.edge,++ node: f.node,++ price_per_gb_usd: f.price_per_gb_usd,++ price_per_request_usd: f.price_per_request_usd,++ cache_rules: f.cache_rules,++ };++ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];++ return record;++}++++/** { ok, errors } for a record against platform.delivery-offer@1. */++function validateDeliveryOffer(record) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.delivery-offer@1', record);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++module.exports = { deliveryOffer, validateDeliveryOffer };nnew file mode 100644@@@ -0,0 +1,98 @@++'use strict';++/**++ * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a Node descriptor to a++ * platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1.++ * Pure construction and validation, no I/O and no dependency at require time.++ *++ * const { nodeCapabilities, nodeOffers, validateNodeCapabilities } = require('openvibe-sdk/node');++ * const caps = nodeCapabilities({ node_id, cpu, arch, memory_mb, storage, network, regions, tags, costs });++ * const { resource, runtime } = nodeOffers(caps);++ */++++/** The fields platform.node-capabilities@1 requires or allows; null/undefined are stripped. */++function nodeCapabilities(fields) {++ const f = fields || {};++ const record = {++ node_id: f.node_id,++ cpu: f.cpu,++ arch: f.arch,++ memory_mb: f.memory_mb,++ gpu: f.gpu,++ storage: f.storage,++ network: f.network,++ regions: f.regions,++ tags: f.tags,++ costs: f.costs,++ capabilities: f.capabilities,++ agent_version: f.agent_version,++ updated_at: f.updated_at,++ };++ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];++ return record;++}++++/**++ * Map a Node descriptor (platform.node-capabilities@1) to two offers:++ * - resource: a platform.resource-offer@1 with kind "node", capabilities including every advertised++ * worker capability prefixed "worker:<name>", trust from opts.trust or "first-party";++ * - runtime: a platform.runtime-offer@1 with kind "container" (the default runtime a node offers),++ * limits from cpu/memory, price from costs.per_hour_usd.++ * opts.trust defaults to "first-party"; it is "user-owned" only when opts.trust says so.++ */++function nodeOffers(descriptor, opts = {}) {++ const d = descriptor || {};++ const trust = opts.trust || 'first-party';++ const caps = Array.isArray(d.capabilities) ? d.capabilities.slice() : [];++ // Add worker:<name> for each worker capability already in the form "worker:<name>"++ // (the descriptor's capabilities field uses the same form as resource-offer).++ const region = Array.isArray(d.regions) && d.regions.length ? d.regions[0] : 'global';++ const now = new Date().toISOString();++++ const resource = {++ offer_id: `node:${d.node_id || 'unknown'}`,++ kind: 'node',++ node_id: d.node_id,++ region,++ trust,++ capabilities: caps,++ capacity: {++ cpu: d.cpu ? { available_cores: d.cpu.cores, utilization: 0 } : undefined,++ memory: d.memory_mb != null ? { available_mb: d.memory_mb } : undefined,++ gpu: d.gpu ? { type: d.gpu.model, vram_free_mb: d.gpu.vram_mb } : undefined,++ disk: d.storage ? { scratch_gb: d.storage.available_gb != null ? d.storage.available_gb : d.storage.capacity_gb } : undefined,++ },++ health: { status: 'up', checked_at: now },++ pricing: d.costs ? { model: 'per-second', marginal_usd_per_unit: (Number(d.costs.per_hour_usd) || 0) / 3600 } : { model: 'prepaid' },++ updated_at: d.updated_at || now,++ };++ // Strip undefined keys from capacity++ if (resource.capacity) {++ for (const k of Object.keys(resource.capacity)) if (resource.capacity[k] == null) delete resource.capacity[k];++ }++++ const cpuCores = d.cpu ? Number(d.cpu.cores) || 1 : 1;++ const memoryMb = Number(d.memory_mb) || 512;++ const runtime = {++ id: `runtime:node:${d.node_id || 'unknown'}`,++ kind: 'container',++ region,++ node: d.node_id,++ limits: { cpu_cores: cpuCores, memory_mb: memoryMb },++ price: { amount_usd: d.costs ? (Number(d.costs.per_hour_usd) || 0) : 0, unit: 'hour' },++ availability: 'available',++ constraints: [trust],++ };++++ return { resource, runtime };++}++++/** { ok, errors } for a record against platform.node-capabilities@1. */++function validateNodeCapabilities(record) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.node-capabilities@1', record);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++module.exports = { nodeCapabilities, nodeOffers, validateNodeCapabilities };@@@ -254,4 +254,7 @@ function createPlanHolder(publicKeys, { now = () => Date.now(), onReject = null};}--module.exports = { plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical };++/** The five trust classes (T1 ADR-046): the order is the planner's preference when no list is named. */++const TRUST_CLASSES = ['first-party', 'user-owned', 'partner', 'community', 'external'];++++module.exports = { plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical, TRUST_CLASSES };nnew file mode 100644@@@ -0,0 +1,55 @@++'use strict';++/**++ * openvibe-sdk/runtime — platform.runtime-offer@1 and platform.runtime-class@1.++ * Pure construction and validation, no I/O and no dependency at require time.++ *++ * const { runtimeOffer, runtimeClass, validateRuntimeOffer, validateRuntimeClass } = require('openvibe-sdk/runtime');++ * const offer = runtimeOffer({ id, kind, region, node, limits, price, availability, constraints });++ * runtimeClass('function'); // 'function' — a known class++ */++++/** The fields platform.runtime-offer@1 requires or allows; null/undefined are stripped. */++function runtimeOffer(fields) {++ const f = fields || {};++ const record = {++ id: f.id,++ kind: f.kind,++ region: f.region,++ node: f.node,++ limits: f.limits,++ price: f.price,++ availability: f.availability,++ constraints: f.constraints,++ };++ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];++ return record;++}++++/** A runtime class is one of the known strings (function, code, browser, linux, desktop, gpu). */++const RUNTIME_CLASSES = ['function', 'code', 'browser', 'linux', 'desktop', 'gpu'];++++/** Returns the class when it is a known runtime class, or throws. */++function runtimeClass(value) {++ if (RUNTIME_CLASSES.includes(value)) return value;++ throw new Error(`runtimeClass: unknown class ${JSON.stringify(value)} (expected one of ${RUNTIME_CLASSES.join(', ')})`);++}++++/** { ok, errors } for a record against platform.runtime-offer@1. */++function validateRuntimeOffer(record) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.runtime-offer@1', record);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++/** { ok, errors } for a value against platform.runtime-class@1. */++function validateRuntimeClass(value) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.runtime-class@1', value);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++module.exports = { runtimeOffer, runtimeClass, RUNTIME_CLASSES, validateRuntimeOffer, validateRuntimeClass };nnew file mode 100644@@@ -0,0 +1,38 @@++'use strict';++/**++ * openvibe-sdk/storage — platform.storage-offer@1.++ * Pure construction and validation, no I/O and no dependency at require time.++ *++ * const { storageOffer, validateStorageOffer } = require('openvibe-sdk/storage');++ * const offer = storageOffer({ id, class, capacity_gb, price_per_gb_month_usd, price_per_operation_usd,++ * region, node, durability, lifecycle_rules });++ */++++/** The fields platform.storage-offer@1 requires or allows; null/undefined are stripped. */++function storageOffer(fields) {++ const f = fields || {};++ const record = {++ id: f.id,++ class: f.class,++ capacity_gb: f.capacity_gb,++ price_per_gb_month_usd: f.price_per_gb_month_usd,++ price_per_operation_usd: f.price_per_operation_usd,++ region: f.region,++ node: f.node,++ durability: f.durability,++ lifecycle_rules: f.lifecycle_rules,++ };++ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];++ return record;++}++++/** { ok, errors } for a record against platform.storage-offer@1. */++function validateStorageOffer(record) {++ let contracts = null;++ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }++ if (!contracts) return { ok: false, errors: [] };++ const r = contracts.validate('platform.storage-offer@1', record);++ return { ok: Boolean(r.valid), errors: r.errors || [] };++}++++module.exports = { storageOffer, validateStorageOffer };@@@ -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', './govern', './identity', './limits', './openre', './placement', './pubsub', './queue', './service', './sso', './telemetry', './testing', './usage', './valkey', './vip']);++ assert.deepEqual(blocked.sort(), ['./cache', './cost', './db', './delivery', './events', './govern', './identity', './limits', './node', './openre', './placement', './pubsub', './queue', './runtime', './service', './sso', './storage', './telemetry', './testing', './usage', './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,78 @@++'use strict';++/** openvibe-sdk/cost: platform.rate-card@1 and platform.cost-snapshot@1 — build, validate, estimate. */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { run } = require('./helpers');++const { rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot } = require('../src/cost');++++const FIXTURES_RC = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.rate-card');++const FIXTURES_CS = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.cost-snapshot');++let contracts = null;++try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }++if (contracts && ![FIXTURES_RC, FIXTURES_CS].every((dir) =>++ fs.existsSync(path.join(dir, 'valid')) && fs.existsSync(path.join(dir, 'invalid')))) contracts = null;++++run([++ ['rateCard passes every valid fixture through unchanged, and they validate', async () => {++ if (!contracts) { console.log('rate-card fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'valid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'valid', f), 'utf8'));++ assert.deepEqual(rateCard({ ...fixture }), fixture, f);++ assert.deepEqual(validateRateCard(fixture), { ok: true, errors: [] }, f);++ }++ }],++ ['rateCard strips null/undefined and keeps zero values', async () => {++ const r = rateCard({ id: 'rc', provider: 'p', metric: 'm', unit_size: 1, unit_price_usd: 0,++ free_allowance: 0, reset_period: 'month', effective_from: '2026-09-01',++ source: 'https://example.com', verified_at: '2026-09-28', adapter: null, region: undefined });++ assert.ok(!('adapter' in r), 'null adapter stripped');++ assert.ok(!('region' in r), 'undefined region stripped');++ assert.equal(r.unit_price_usd, 0, 'a 0 is kept, not stripped');++ }],++ ['validateRateCard refuses every invalid fixture with its errors', async () => {++ if (!contracts) { console.log('rate-card invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'invalid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'invalid', f), 'utf8'));++ const r = validateRateCard(fixture);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++ ['costSnapshot passes every valid fixture through unchanged, and they validate', async () => {++ if (!contracts) { console.log('cost-snapshot fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_CS, 'valid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_CS, 'valid', f), 'utf8'));++ assert.deepEqual(costSnapshot({ ...fixture }), fixture, f);++ assert.deepEqual(validateCostSnapshot(fixture), { ok: true, errors: [] }, f);++ }++ }],++ ['costSnapshot strips null/undefined', async () => {++ const s = costSnapshot({ window_start: '2026-09-28T00:00:00Z', window_end: '2026-09-29T00:00:00Z',++ scope: 'events', by_target: [], actual_usd: 0, project_id: null, counterfactual_usd: undefined });++ assert.ok(!('project_id' in s));++ assert.ok(!('counterfactual_usd' in s));++ }],++ ['validateCostSnapshot refuses every invalid fixture with its errors', async () => {++ if (!contracts) { console.log('cost-snapshot invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_CS, 'invalid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_CS, 'invalid', f), 'utf8'));++ const r = validateCostSnapshot(fixture);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++ ['estimate: free allowance shrinks billable, unit_size prorate, cost_usd is right', async () => {++ const card = rateCard({ id: 'rc', provider: 'p', metric: 'm', unit_size: 1000, unit_price_usd: 0.5,++ free_allowance: 500, reset_period: 'month', effective_from: '2026-09-01',++ source: 'https://example.com', verified_at: '2026-09-28' });++ // 300 units: under the free allowance → billable = 0++ assert.deepEqual(estimate(card, 300, 'ops'), { quantity: 300, unit: 'ops', billable: 0, cost_usd: 0 });++ // 1500 units: 1000 billable, at 0.5 per 1000 → 0.5++ const e = estimate(card, 1500, 'ops');++ assert.equal(e.billable, 1000);++ assert.equal(e.cost_usd, 0.5);++ // null unit is carried through as null++ assert.equal(estimate(card, 100).unit, null);++ }],++]);nnew file mode 100644@@@ -0,0 +1,38 @@++'use strict';++/** openvibe-sdk/delivery: platform.delivery-offer@1 — build and validate. */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { run } = require('./helpers');++const { deliveryOffer, validateDeliveryOffer } = require('../src/delivery');++++const FIXTURES = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.delivery-offer');++let contracts = null;++try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }++if (contracts && (!fs.existsSync(path.join(FIXTURES, 'valid')) || !fs.existsSync(path.join(FIXTURES, 'invalid')))) contracts = null;++++run([++ ['deliveryOffer passes every valid fixture through unchanged, and they validate', async () => {++ if (!contracts) { console.log('delivery-offer fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES, 'valid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'valid', f), 'utf8'));++ assert.deepEqual(deliveryOffer({ ...fixture }), fixture, f);++ assert.deepEqual(validateDeliveryOffer(fixture), { ok: true, errors: [] }, f);++ }++ }],++ ['deliveryOffer strips null/undefined', async () => {++ const o = deliveryOffer({ id: 'd1', transports: ['http'], regions: ['us-west'], edge: true,++ price_per_gb_usd: 0.02, price_per_request_usd: 0.00001,++ cache_rules: { enabled: true }, node: null });++ assert.ok(!('node' in o), 'null node stripped');++ }],++ ['validateDeliveryOffer refuses every invalid fixture with its errors', async () => {++ if (!contracts) { console.log('delivery-offer invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES, 'invalid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'invalid', f), 'utf8'));++ const r = validateDeliveryOffer(fixture);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++]);nnew file mode 100644@@@ -0,0 +1,81 @@++'use strict';++/** openvibe-sdk/node: platform.node-capabilities@1 — build, validate, and nodeOffers(). */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { run } = require('./helpers');++const { nodeCapabilities, nodeOffers, validateNodeCapabilities } = require('../src/node');++++const FIXTURES = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.node-capabilities');++let contracts = null;++try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }++if (contracts && (!fs.existsSync(path.join(FIXTURES, 'valid')) || !fs.existsSync(path.join(FIXTURES, 'invalid')))) contracts = null;++++run([++ ['nodeCapabilities passes every valid fixture through unchanged, and they validate', async () => {++ if (!contracts) { console.log('node-capabilities fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES, 'valid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'valid', f), 'utf8'));++ assert.deepEqual(nodeCapabilities({ ...fixture }), fixture, f);++ assert.deepEqual(validateNodeCapabilities(fixture), { ok: true, errors: [] }, f);++ }++ }],++ ['nodeCapabilities strips null/undefined', async () => {++ const c = nodeCapabilities({ node_id: 'n1', cpu: { cores: 4 }, arch: 'x86_64', memory_mb: 8192,++ storage: { capacity_gb: 100 }, network: { ingress_mbps: 100, egress_mbps: 100 },++ regions: ['us-west'], tags: [], costs: { per_hour_usd: 0.1 },++ gpu: null, capabilities: undefined, agent_version: null });++ assert.ok(!('gpu' in c), 'null gpu stripped');++ assert.ok(!('capabilities' in c), 'undefined capabilities stripped');++ assert.ok(!('agent_version' in c), 'null agent_version stripped');++ }],++ ['validateNodeCapabilities refuses every invalid fixture with its errors', async () => {++ if (!contracts) { console.log('node-capabilities invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES, 'invalid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'invalid', f), 'utf8'));++ const r = validateNodeCapabilities(fixture);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++ ['nodeOffers: default trust is first-party, capabilities pass through, kind is "node"', async () => {++ const caps = nodeCapabilities({ node_id: 'n1', cpu: { cores: 8 }, arch: 'x86_64', memory_mb: 16384,++ storage: { capacity_gb: 500, available_gb: 300 }, network: { ingress_mbps: 1000, egress_mbps: 1000 },++ regions: ['us-west'], tags: ['gpu'], costs: { per_hour_usd: 0.2 },++ capabilities: ['node:http', 'worker:ffmpeg'] });++ const { resource, runtime } = nodeOffers(caps);++ assert.equal(resource.kind, 'node');++ assert.equal(resource.trust, 'first-party');++ assert.equal(resource.node_id, 'n1');++ assert.equal(resource.region, 'us-west');++ assert.deepEqual(resource.capabilities, ['node:http', 'worker:ffmpeg']);++ assert.equal(resource.offer_id, 'node:n1');++ assert.equal(resource.health.status, 'up');++ // runtime++ assert.equal(runtime.kind, 'container');++ assert.equal(runtime.node, 'n1');++ assert.deepEqual(runtime.limits, { cpu_cores: 8, memory_mb: 16384 });++ assert.equal(runtime.price.amount_usd, 0.2);++ assert.equal(runtime.price.unit, 'hour');++ assert.deepEqual(runtime.constraints, ['first-party']);++ }],++ ['nodeOffers: trust is "user-owned" only when opts.trust says so', async () => {++ const caps = nodeCapabilities({ node_id: 'n2', cpu: { cores: 4 }, arch: 'arm64', memory_mb: 8192,++ storage: { capacity_gb: 200 }, network: { ingress_mbps: 500, egress_mbps: 500 },++ regions: ['eu-west'], tags: [], costs: { per_hour_usd: 0.1 } });++ const { resource, runtime } = nodeOffers(caps, { trust: 'user-owned' });++ assert.equal(resource.trust, 'user-owned');++ assert.deepEqual(runtime.constraints, ['user-owned']);++ // default (no opts) stays first-party++ const { resource: r2 } = nodeOffers(caps);++ assert.equal(r2.trust, 'first-party');++ }],++ ['nodeOffers: per-second pricing from per_hour_usd', async () => {++ const caps = nodeCapabilities({ node_id: 'n3', cpu: { cores: 2 }, arch: 'x86_64', memory_mb: 4096,++ storage: { capacity_gb: 100 }, network: { ingress_mbps: 100, egress_mbps: 100 },++ regions: ['us-east'], tags: [], costs: { per_hour_usd: 0.36 } });++ const { resource } = nodeOffers(caps);++ assert.equal(resource.pricing.model, 'per-second');++ assert.ok(Math.abs(resource.pricing.marginal_usd_per_unit - 0.0001) < 1e-12); // 0.36 / 3600++ }],++]);@@@ -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, 34);++ assert.equal(subpaths.length, 39);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,59 @@++'use strict';++/** openvibe-sdk/runtime: platform.runtime-offer@1 and platform.runtime-class@1 — build and validate. */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { run } = require('./helpers');++const { runtimeOffer, runtimeClass, RUNTIME_CLASSES, validateRuntimeOffer, validateRuntimeClass } = require('../src/runtime');++++const FIXTURES_RO = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.runtime-offer');++const FIXTURES_RC = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.runtime-class');++let contracts = null;++try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }++if (contracts && ![FIXTURES_RO, FIXTURES_RC].every((dir) =>++ fs.existsSync(path.join(dir, 'valid')) && fs.existsSync(path.join(dir, 'invalid')))) contracts = null;++++run([++ ['runtimeOffer passes every valid fixture through unchanged, and they validate', async () => {++ if (!contracts) { console.log('runtime-offer fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_RO, 'valid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RO, 'valid', f), 'utf8'));++ assert.deepEqual(runtimeOffer({ ...fixture }), fixture, f);++ assert.deepEqual(validateRuntimeOffer(fixture), { ok: true, errors: [] }, f);++ }++ }],++ ['runtimeOffer strips null/undefined', async () => {++ const o = runtimeOffer({ id: 'r1', kind: 'container', region: 'us-west', node: 'n1',++ limits: { cpu_cores: 2, memory_mb: 4096 }, price: { amount_usd: 0.05, unit: 'hour' },++ availability: null, constraints: undefined });++ assert.ok(!('availability' in o), 'null availability stripped');++ assert.ok(!('constraints' in o), 'undefined constraints stripped');++ }],++ ['validateRuntimeOffer refuses every invalid fixture with its errors', async () => {++ if (!contracts) { console.log('runtime-offer invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_RO, 'invalid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RO, 'invalid', f), 'utf8'));++ const r = validateRuntimeOffer(fixture);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++ ['runtimeClass accepts every valid fixture and rejects every invalid one', async () => {++ if (!contracts) { console.log('runtime-class fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'valid'))) {++ const value = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'valid', f), 'utf8'));++ assert.equal(runtimeClass(value), value, f);++ assert.deepEqual(validateRuntimeClass(value), { ok: true, errors: [] }, f);++ }++ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'invalid'))) {++ const value = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'invalid', f), 'utf8'));++ assert.throws(() => runtimeClass(value), /unknown class/);++ const r = validateRuntimeClass(value);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++ ['RUNTIME_CLASSES enumerates the known classes', async () => {++ assert.deepEqual(RUNTIME_CLASSES, ['function', 'code', 'browser', 'linux', 'desktop', 'gpu']);++ }],++]);nnew file mode 100644@@@ -0,0 +1,38 @@++'use strict';++/** openvibe-sdk/storage: platform.storage-offer@1 — build and validate. */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { run } = require('./helpers');++const { storageOffer, validateStorageOffer } = require('../src/storage');++++const FIXTURES = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.storage-offer');++let contracts = null;++try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }++if (contracts && (!fs.existsSync(path.join(FIXTURES, 'valid')) || !fs.existsSync(path.join(FIXTURES, 'invalid')))) contracts = null;++++run([++ ['storageOffer passes every valid fixture through unchanged, and they validate', async () => {++ if (!contracts) { console.log('storage-offer fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES, 'valid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'valid', f), 'utf8'));++ assert.deepEqual(storageOffer({ ...fixture }), fixture, f);++ assert.deepEqual(validateStorageOffer(fixture), { ok: true, errors: [] }, f);++ }++ }],++ ['storageOffer strips null/undefined', async () => {++ const o = storageOffer({ id: 's1', class: 'warm', capacity_gb: 1000, price_per_gb_month_usd: 0.02,++ price_per_operation_usd: 0.00001, region: 'us-west', node: 'n1',++ durability: { replicas: 3 }, lifecycle_rules: null });++ assert.ok(!('lifecycle_rules' in o), 'null lifecycle_rules stripped');++ }],++ ['validateStorageOffer refuses every invalid fixture with its errors', async () => {++ if (!contracts) { console.log('storage-offer invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }++ for (const f of fs.readdirSync(path.join(FIXTURES, 'invalid'))) {++ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'invalid', f), 'utf8'));++ const r = validateStorageOffer(fixture);++ assert.equal(r.ok, false, f);++ assert.ok(r.errors.length > 0, f);++ }++ }],++]);nnew file mode 100644@@@ -0,0 +1,17 @@++/** platform.rate-card@1 and platform.cost-snapshot@1: provider pricing and cost windows. Validated by++ * openvibe-contracts (required lazily by the validate* functions). */++export type RateCard = { id: string; provider: string; adapter?: string; metric: string; unit_size: number;++ unit_price_usd: number; free_allowance: number; reset_period: 'day' | 'month' | 'none'; region?: string;++ effective_from: string; effective_until?: string; source: string; verified_at: string };++export type CostSnapshotTarget = { id: string; units: number; cost_usd: number; p50_ms?: number; p95_ms?: number };++export type CostSnapshot = { window_start: string; window_end: string; project_id?: string; scope: string;++ by_target: CostSnapshotTarget[]; actual_usd: number; counterfactual_usd?: Record<string, number> };++export type EstimateResult = { quantity: number; unit: string | null; billable: number; cost_usd: number };++export type ValidationError = { path: string; message: string };++/** Null/undefined fields are stripped. */++export declare function rateCard(fields: Partial<RateCard>): RateCard;++export declare function costSnapshot(fields: Partial<CostSnapshot>): CostSnapshot;++/** The cost of `quantity` units in one rate card after its free allowance. */++export declare function estimate(card: RateCard, quantity: number, unit?: string): EstimateResult;++export declare function validateRateCard(record: unknown): { ok: boolean; errors: ValidationError[] };++export declare function validateCostSnapshot(record: unknown): { ok: boolean; errors: ValidationError[] };nnew file mode 100644@@@ -0,0 +1,9 @@++/** platform.delivery-offer@1: transport and edge delivery capacity with cache policy and pricing. Validated by++ * openvibe-contracts (required lazily by validateDeliveryOffer). */++export type DeliveryOfferCacheRules = { enabled: boolean; ttl_seconds?: number; max_object_mb?: number };++export type DeliveryOffer = { id: string; transports: ('http' | 'hls' | 'rtmp' | 'srt' | 'ws')[]; regions: string[];++ edge: boolean; node?: string; price_per_gb_usd: number; price_per_request_usd: number; cache_rules: DeliveryOfferCacheRules };++export type ValidationError = { path: string; message: string };++/** Null/undefined fields are stripped. */++export declare function deliveryOffer(fields: Partial<DeliveryOffer>): DeliveryOffer;++export declare function validateDeliveryOffer(record: unknown): { ok: boolean; errors: ValidationError[] };nnew file mode 100644@@@ -0,0 +1,20 @@++/** platform.node-capabilities@1: node hardware, placement regions, tags and operating costs. Validated by++ * openvibe-contracts (required lazily by validateNodeCapabilities). nodeOffers() maps a descriptor to a++ * platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1. */++export type NodeCpu = { cores: number; threads?: number };++export type NodeGpu = { model: string; count: number; vram_mb?: number };++export type NodeStorage = { capacity_gb: number; available_gb?: number; kind?: string };++export type NodeNetwork = { ingress_mbps: number; egress_mbps: number };++export type NodeCosts = { per_hour_usd: number; egress_per_gb_usd?: number };++export type NodeCapabilities = { node_id: string; cpu: NodeCpu; arch: string; memory_mb: number; gpu?: NodeGpu;++ storage: NodeStorage; network: NodeNetwork; regions: string[]; tags: string[]; costs: NodeCosts;++ capabilities?: string[]; agent_version?: string; updated_at?: string };++export type NodeOffersOptions = { trust?: string };++export type ValidationError = { path: string; message: string };++/** Null/undefined fields are stripped. */++export declare function nodeCapabilities(fields: Partial<NodeCapabilities>): NodeCapabilities;++/** Map a Node descriptor to { resource: resource-offer@1, runtime: runtime-offer@1 }. trust defaults to++ * "first-party"; it is "user-owned" only when opts.trust says so. */++export declare function nodeOffers(descriptor: NodeCapabilities, opts?: NodeOffersOptions): {++ resource: Record<string, unknown>; runtime: Record<string, unknown> };++export declare function validateNodeCapabilities(record: unknown): { ok: boolean; errors: ValidationError[] };@@@ -24,3 +24,5 @@ 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;++/** The five trust classes (T1 ADR-046): first-party, user-owned, partner, community, external. */++export declare const TRUST_CLASSES: readonly ['first-party', 'user-owned', 'partner', 'community', 'external'];nnew file mode 100644@@@ -0,0 +1,16 @@++/** platform.runtime-offer@1 and platform.runtime-class@1: execution capacity and environment kinds. Validated++ * by openvibe-contracts (required lazily by the validate* functions). */++export type RuntimeOfferLimits = { cpu_cores: number; memory_mb: number; duration_seconds?: number; concurrency?: number };++export type RuntimeOfferPrice = { amount_usd: number; unit: 'second' | 'hour' | 'invocation' };++export type RuntimeOffer = { id: string; kind: 'container' | 'process' | 'wasm' | 'vm' | 'function'; region: string;++ node: string; limits: RuntimeOfferLimits; price: RuntimeOfferPrice; availability: 'available' | 'limited' | 'unavailable';++ constraints: string[] };++export type RuntimeClass = 'function' | 'code' | 'browser' | 'linux' | 'desktop' | 'gpu';++export type ValidationError = { path: string; message: string };++/** Null/undefined fields are stripped. */++export declare function runtimeOffer(fields: Partial<RuntimeOffer>): RuntimeOffer;++/** Returns the class when it is a known runtime class, or throws. */++export declare function runtimeClass(value: string): RuntimeClass;++export declare const RUNTIME_CLASSES: RuntimeClass[];++export declare function validateRuntimeOffer(record: unknown): { ok: boolean; errors: ValidationError[] };++export declare function validateRuntimeClass(value: unknown): { ok: boolean; errors: ValidationError[] };nnew file mode 100644@@@ -0,0 +1,11 @@++/** platform.storage-offer@1: durable storage capacity and operation pricing. Validated by openvibe-contracts++ * (required lazily by validateStorageOffer). */++export type StorageOfferDurability = { replicas: number; target_nines?: number };++export type StorageOfferLifecycleRule = { after_days: number; action: 'transition' | 'expire'; target_class?: 'hot' | 'warm' | 'cold' };++export type StorageOffer = { id: string; class: 'hot' | 'warm' | 'cold'; capacity_gb: number;++ price_per_gb_month_usd: number; price_per_operation_usd: number; region: string; node: string;++ durability: StorageOfferDurability; lifecycle_rules: StorageOfferLifecycleRule[] };++export type ValidationError = { path: string; message: string };++/** Null/undefined fields are stripped. */++export declare function storageOffer(fields: Partial<StorageOffer>): StorageOffer;++export declare function validateStorageOffer(record: unknown): { ok: boolean; errors: ValidationError[] };
@@ -7,8 +7,10 @@ release may change an API and says so here.
@@ -7,8 +7,10 @@ release may change an API and says so here.
-## Unreleased
+## 0.27.0 (2026-10-03)
+- Add server subpaths `openvibe-sdk/cost`, `openvibe-sdk/runtime`, `openvibe-sdk/storage`,
+ `openvibe-sdk/delivery`, and `openvibe-sdk/node` for Fabric contract records and offer builders.
- **`openvibe-sdk/usage` (new, server; plan T1):** `platform.usage-sample@1` readings in one place.
- **`openvibe-sdk/usage` (new, server; plan T1):** `platform.usage-sample@1` readings in one place.
`usageSample(fields)` builds the record — `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit` and
`usageSample(fields)` builds the record — `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit` and
every optional dimension pass through, `at` defaults to now (ISO) and `source` to `openvibe-sdk/usage`, null fields
every optional dimension pass through, `at` defaults to now (ISO) and `source` to `openvibe-sdk/usage`, null fields
@@ -183,6 +183,11 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -183,6 +183,11 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/vip` | server | `createVipClient({baseUrl?, tokenClient \| getToken, fetch?, timeoutMs?})`: `evaluate({subject, resource, owner, fallback, mode})`, `checkEntitlement({subject, creator, product, mode})`, `isMember(subject, creator)`; every failure is a denial. `createVipCache({vip, ttlMs, denyTtlMs, unavailableTtlMs})`: `entitlement`, `peekEntitlement`, `evaluate`, `invalidate`, `handleEvent` (`vip.membership.changed` and Billing's entitlement events drop a member's answers at once), `clear`, `bounds` |
| `openvibe-sdk/vip` | server | `createVipClient({baseUrl?, tokenClient \| getToken, fetch?, timeoutMs?})`: `evaluate({subject, resource, owner, fallback, mode})`, `checkEntitlement({subject, creator, product, mode})`, `isMember(subject, creator)`; every failure is a denial. `createVipCache({vip, ttlMs, denyTtlMs, unavailableTtlMs})`: `entitlement`, `peekEntitlement`, `evaluate`, `invalidate`, `handleEvent` (`vip.membership.changed` and Billing's entitlement events drop a member's answers at once), `clear`, `bounds` |
| `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `openvibe-sdk/govern` builds its `onUsage` record with it |
| `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `openvibe-sdk/govern` builds its `onUsage` record with it |
| `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute |
| `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute |
+| `openvibe-sdk/cost` | server | Build and validate `platform.rate-card@1` and `platform.cost-snapshot@1` records with `rateCard()`, `costSnapshot()`, `validateRateCard()`, and `validateCostSnapshot()`; `estimate()` calculates a charge from a rate card |
+| `openvibe-sdk/runtime` | server | Build and validate `platform.runtime-offer@1` records with `runtimeOffer()` and `validateRuntimeOffer()`; `runtimeClass()`, `RUNTIME_CLASSES`, and `validateRuntimeClass()` handle known runtime classes |
+| `openvibe-sdk/storage` | server | `storageOffer()` and `validateStorageOffer()` for `platform.storage-offer@1` |
+| `openvibe-sdk/delivery` | server | `deliveryOffer()` and `validateDeliveryOffer()` for `platform.delivery-offer@1` |
+| `openvibe-sdk/node` | server | `nodeCapabilities()` and `validateNodeCapabilities()` for `platform.node-capabilities@1`; `nodeOffers()` derives resource and runtime offers from a node descriptor |
| `openvibe-sdk/service` | server | the service kit: `gracefulStop({name, server, stop, close, drainMs, deadlineMs, deadlineExitCode, beforeDrain, handles})` → `{stop, stopping}`, `within(ms, p)`; `createServiceError(name)`, `ServiceError`, `asServiceError`, `run`, `wrap`, `sendError`, `jsonBody({limit})`, `privateNoStore`, `jsonErrors()`; re-exports (loaded on first use) of openvibe-shared `createReadiness`, `skip`, `safeReason`, `createRegistry`, `instrument`, `metricsHandler`, `isLoopbackDirect`, `releaseInfo`, `createRelease` and openvibe-contracts `problem`, `sendProblem` |
| `openvibe-sdk/service` | server | the service kit: `gracefulStop({name, server, stop, close, drainMs, deadlineMs, deadlineExitCode, beforeDrain, handles})` → `{stop, stopping}`, `within(ms, p)`; `createServiceError(name)`, `ServiceError`, `asServiceError`, `run`, `wrap`, `sendError`, `jsonBody({limit})`, `privateNoStore`, `jsonErrors()`; re-exports (loaded on first use) of openvibe-shared `createReadiness`, `skip`, `safeReason`, `createRegistry`, `instrument`, `metricsHandler`, `isLoopbackDirect`, `releaseInfo`, `createRelease` and openvibe-contracts `problem`, `sendProblem` |
| `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media, Tools jobs and the Tools platform API on an in-process `fetch` |
| `openvibe-sdk/testing` | Node | `createMockPlatform()`: fake Network (incl. developer apps and projects), Events, Media, Tools jobs and the Tools platform API on an in-process `fetch` |
| `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, tools, projects |
| `openvibe-sdk/browser/openvibe-sdk.mjs` | browser | one self-contained ES module: core + auth (browser), registry, modules, realtime, media, community, jobs, tools, projects |
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.26.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.27.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.26.0';
+const SDK_VERSION = '0.27.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/cost.js. Do not edit.
+import cjs from '../src/cost.js';
+
+export const {
+ rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot,
+} = cjs;
+export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/delivery.js. Do not edit.
+import cjs from '../src/delivery.js';
+
+export const {
+ deliveryOffer, validateDeliveryOffer,
+} = cjs;
+export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/node.js. Do not edit.
+import cjs from '../src/node.js';
+
+export const {
+ nodeCapabilities, nodeOffers, validateNodeCapabilities,
+} = cjs;
+export default cjs;
@@ -4,5 +4,6 @@ import cjs from '../src/placement.js';
@@ -4,5 +4,6 @@ import cjs from '../src/placement.js';
export const {
export const {
plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost,
plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost,
rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical,
rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical,
+ TRUST_CLASSES,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/runtime.js. Do not edit.
+import cjs from '../src/runtime.js';
+
+export const {
+ runtimeOffer, runtimeClass, RUNTIME_CLASSES, validateRuntimeOffer, validateRuntimeClass,
+} = cjs;
+export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/storage.js. Do not edit.
+import cjs from '../src/storage.js';
+
+export const {
+ storageOffer, validateStorageOffer,
+} = cjs;
+export default cjs;
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.26.0",
+ "version": "0.27.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.26.0",
+ "version": "0.27.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.26.0",
+ "version": "0.27.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",
@@ -106,6 +106,36 @@
@@ -106,6 +106,36 @@
"import": "./esm/telemetry.mjs",
"import": "./esm/telemetry.mjs",
"default": "./src/telemetry.js"
"default": "./src/telemetry.js"
},
},
+ "./cost": {
+ "types": "./types/cost.d.ts",
+ "browser": null,
+ "import": "./esm/cost.mjs",
+ "default": "./src/cost.js"
+ },
+ "./runtime": {
+ "types": "./types/runtime.d.ts",
+ "browser": null,
+ "import": "./esm/runtime.mjs",
+ "default": "./src/runtime.js"
+ },
+ "./storage": {
+ "types": "./types/storage.d.ts",
+ "browser": null,
+ "import": "./esm/storage.mjs",
+ "default": "./src/storage.js"
+ },
+ "./delivery": {
+ "types": "./types/delivery.d.ts",
+ "browser": null,
+ "import": "./esm/delivery.mjs",
+ "default": "./src/delivery.js"
+ },
+ "./node": {
+ "types": "./types/node.d.ts",
+ "browser": null,
+ "import": "./esm/node.mjs",
+ "default": "./src/node.js"
+ },
"./db": {
"./db": {
"types": "./types/db.d.ts",
"types": "./types/db.d.ts",
"browser": null,
"browser": null,
@@ -30,6 +30,11 @@ const ENTRIES = {
@@ -30,6 +30,11 @@ const ENTRIES = {
'placement.mjs': '../src/placement.js',
'placement.mjs': '../src/placement.js',
'usage.mjs': '../src/usage.js',
'usage.mjs': '../src/usage.js',
'telemetry.mjs': '../src/telemetry.js',
'telemetry.mjs': '../src/telemetry.js',
+ 'cost.mjs': '../src/cost.js',
+ 'runtime.mjs': '../src/runtime.js',
+ 'storage.mjs': '../src/storage.js',
+ 'delivery.mjs': '../src/delivery.js',
+ 'node.mjs': '../src/node.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.26.0';
+const SDK_VERSION = '0.27.0';
module.exports = {
module.exports = {
createClient,
createClient,
new file mode 100644
new file mode 100644
@@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
+'use strict';
+/**
+ * openvibe-sdk/cost — platform.rate-card@1 and platform.cost-snapshot@1, plus estimate() for a rate card.
+ * Pure construction and validation, no I/O and no dependency at require time: openvibe-contracts is required
+ * lazily by the validate* functions, the way src/service re-exports its packages.
+ *
+ * const { rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot } = require('openvibe-sdk/cost');
+ * const card = rateCard({ id, provider, metric, unit_size, unit_price_usd, free_allowance, reset_period,
+ * effective_from, source, verified_at });
+ * estimate(card, 2_500_000, 'ops'); // { quantity, unit, billable, cost_usd }
+ */
+
+/** The fields platform.rate-card@1 requires or allows, in the schema's order; null/undefined are stripped. */
+function rateCard(fields) {
+ const f = fields || {};
+ const record = {
+ id: f.id,
+ provider: f.provider,
+ adapter: f.adapter,
+ metric: f.metric,
+ unit_size: f.unit_size,
+ unit_price_usd: f.unit_price_usd,
+ free_allowance: f.free_allowance,
+ reset_period: f.reset_period,
+ region: f.region,
+ effective_from: f.effective_from,
+ effective_until: f.effective_until,
+ source: f.source,
+ verified_at: f.verified_at,
+ };
+ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];
+ return record;
+}
+
+/** The fields platform.cost-snapshot@1 requires or allows; null/undefined are stripped. */
+function costSnapshot(fields) {
+ const f = fields || {};
+ const record = {
+ window_start: f.window_start,
+ window_end: f.window_end,
+ project_id: f.project_id,
+ scope: f.scope,
+ by_target: f.by_target,
+ actual_usd: f.actual_usd,
+ counterfactual_usd: f.counterfactual_usd,
+ };
+ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];
+ return record;
+}
+
+/**
+ * The cost of `quantity` units in one rate card: billable = max(0, quantity − free_allowance), priced in whole
+ * unit_size chunks at unit_price_usd. `unit` is carried through for the caller's label; the math is unit-agnostic.
+ */
+function estimate(card, quantity, unit) {
+ const q = Number(quantity) || 0;
+ const free = Number(card.free_allowance) || 0;
+ const billable = Math.max(0, q - free);
+ const size = Number(card.unit_size) || 1;
+ const cost_usd = (billable / size) * (Number(card.unit_price_usd) || 0);
+ return { quantity: q, unit: unit || null, billable, cost_usd };
+}
+
+/** { ok, errors } for a record against platform.rate-card@1. */
+function validateRateCard(record) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.rate-card@1', record);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+/** { ok, errors } for a record against platform.cost-snapshot@1. */
+function validateCostSnapshot(record) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.cost-snapshot@1', record);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+module.exports = { rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot };
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
+'use strict';
+/**
+ * openvibe-sdk/delivery — platform.delivery-offer@1.
+ * Pure construction and validation, no I/O and no dependency at require time.
+ *
+ * const { deliveryOffer, validateDeliveryOffer } = require('openvibe-sdk/delivery');
+ * const offer = deliveryOffer({ id, transports, regions, edge, price_per_gb_usd, price_per_request_usd,
+ * cache_rules });
+ */
+
+/** The fields platform.delivery-offer@1 requires or allows; null/undefined are stripped. */
+function deliveryOffer(fields) {
+ const f = fields || {};
+ const record = {
+ id: f.id,
+ transports: f.transports,
+ regions: f.regions,
+ edge: f.edge,
+ node: f.node,
+ price_per_gb_usd: f.price_per_gb_usd,
+ price_per_request_usd: f.price_per_request_usd,
+ cache_rules: f.cache_rules,
+ };
+ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];
+ return record;
+}
+
+/** { ok, errors } for a record against platform.delivery-offer@1. */
+function validateDeliveryOffer(record) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.delivery-offer@1', record);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+module.exports = { deliveryOffer, validateDeliveryOffer };
new file mode 100644
new file mode 100644
@@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
+'use strict';
+/**
+ * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a Node descriptor to a
+ * platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1.
+ * Pure construction and validation, no I/O and no dependency at require time.
+ *
+ * const { nodeCapabilities, nodeOffers, validateNodeCapabilities } = require('openvibe-sdk/node');
+ * const caps = nodeCapabilities({ node_id, cpu, arch, memory_mb, storage, network, regions, tags, costs });
+ * const { resource, runtime } = nodeOffers(caps);
+ */
+
+/** The fields platform.node-capabilities@1 requires or allows; null/undefined are stripped. */
+function nodeCapabilities(fields) {
+ const f = fields || {};
+ const record = {
+ node_id: f.node_id,
+ cpu: f.cpu,
+ arch: f.arch,
+ memory_mb: f.memory_mb,
+ gpu: f.gpu,
+ storage: f.storage,
+ network: f.network,
+ regions: f.regions,
+ tags: f.tags,
+ costs: f.costs,
+ capabilities: f.capabilities,
+ agent_version: f.agent_version,
+ updated_at: f.updated_at,
+ };
+ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];
+ return record;
+}
+
+/**
+ * Map a Node descriptor (platform.node-capabilities@1) to two offers:
+ * - resource: a platform.resource-offer@1 with kind "node", capabilities including every advertised
+ * worker capability prefixed "worker:<name>", trust from opts.trust or "first-party";
+ * - runtime: a platform.runtime-offer@1 with kind "container" (the default runtime a node offers),
+ * limits from cpu/memory, price from costs.per_hour_usd.
+ * opts.trust defaults to "first-party"; it is "user-owned" only when opts.trust says so.
+ */
+function nodeOffers(descriptor, opts = {}) {
+ const d = descriptor || {};
+ const trust = opts.trust || 'first-party';
+ const caps = Array.isArray(d.capabilities) ? d.capabilities.slice() : [];
+ // Add worker:<name> for each worker capability already in the form "worker:<name>"
+ // (the descriptor's capabilities field uses the same form as resource-offer).
+ const region = Array.isArray(d.regions) && d.regions.length ? d.regions[0] : 'global';
+ const now = new Date().toISOString();
+
+ const resource = {
+ offer_id: `node:${d.node_id || 'unknown'}`,
+ kind: 'node',
+ node_id: d.node_id,
+ region,
+ trust,
+ capabilities: caps,
+ capacity: {
+ cpu: d.cpu ? { available_cores: d.cpu.cores, utilization: 0 } : undefined,
+ memory: d.memory_mb != null ? { available_mb: d.memory_mb } : undefined,
+ gpu: d.gpu ? { type: d.gpu.model, vram_free_mb: d.gpu.vram_mb } : undefined,
+ disk: d.storage ? { scratch_gb: d.storage.available_gb != null ? d.storage.available_gb : d.storage.capacity_gb } : undefined,
+ },
+ health: { status: 'up', checked_at: now },
+ pricing: d.costs ? { model: 'per-second', marginal_usd_per_unit: (Number(d.costs.per_hour_usd) || 0) / 3600 } : { model: 'prepaid' },
+ updated_at: d.updated_at || now,
+ };
+ // Strip undefined keys from capacity
+ if (resource.capacity) {
+ for (const k of Object.keys(resource.capacity)) if (resource.capacity[k] == null) delete resource.capacity[k];
+ }
+
+ const cpuCores = d.cpu ? Number(d.cpu.cores) || 1 : 1;
+ const memoryMb = Number(d.memory_mb) || 512;
+ const runtime = {
+ id: `runtime:node:${d.node_id || 'unknown'}`,
+ kind: 'container',
+ region,
+ node: d.node_id,
+ limits: { cpu_cores: cpuCores, memory_mb: memoryMb },
+ price: { amount_usd: d.costs ? (Number(d.costs.per_hour_usd) || 0) : 0, unit: 'hour' },
+ availability: 'available',
+ constraints: [trust],
+ };
+
+ return { resource, runtime };
+}
+
+/** { ok, errors } for a record against platform.node-capabilities@1. */
+function validateNodeCapabilities(record) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.node-capabilities@1', record);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+module.exports = { nodeCapabilities, nodeOffers, validateNodeCapabilities };
@@ -254,4 +254,7 @@ function createPlanHolder(publicKeys, { now = () => Date.now(), onReject = null
@@ -254,4 +254,7 @@ function createPlanHolder(publicKeys, { now = () => Date.now(), onReject = null
};
};
}
}
-module.exports = { plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical };
+/** The five trust classes (T1 ADR-046): the order is the planner's preference when no list is named. */
+const TRUST_CLASSES = ['first-party', 'user-owned', 'partner', 'community', 'external'];
+
+module.exports = { plan, excluded, marginalCost, priceOf, forecast, ownedLoadCost, rendezvous, pickTwo, signPlan, verifyPlan, createPlanHolder, canonical, TRUST_CLASSES };
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+'use strict';
+/**
+ * openvibe-sdk/runtime — platform.runtime-offer@1 and platform.runtime-class@1.
+ * Pure construction and validation, no I/O and no dependency at require time.
+ *
+ * const { runtimeOffer, runtimeClass, validateRuntimeOffer, validateRuntimeClass } = require('openvibe-sdk/runtime');
+ * const offer = runtimeOffer({ id, kind, region, node, limits, price, availability, constraints });
+ * runtimeClass('function'); // 'function' — a known class
+ */
+
+/** The fields platform.runtime-offer@1 requires or allows; null/undefined are stripped. */
+function runtimeOffer(fields) {
+ const f = fields || {};
+ const record = {
+ id: f.id,
+ kind: f.kind,
+ region: f.region,
+ node: f.node,
+ limits: f.limits,
+ price: f.price,
+ availability: f.availability,
+ constraints: f.constraints,
+ };
+ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];
+ return record;
+}
+
+/** A runtime class is one of the known strings (function, code, browser, linux, desktop, gpu). */
+const RUNTIME_CLASSES = ['function', 'code', 'browser', 'linux', 'desktop', 'gpu'];
+
+/** Returns the class when it is a known runtime class, or throws. */
+function runtimeClass(value) {
+ if (RUNTIME_CLASSES.includes(value)) return value;
+ throw new Error(`runtimeClass: unknown class ${JSON.stringify(value)} (expected one of ${RUNTIME_CLASSES.join(', ')})`);
+}
+
+/** { ok, errors } for a record against platform.runtime-offer@1. */
+function validateRuntimeOffer(record) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.runtime-offer@1', record);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+/** { ok, errors } for a value against platform.runtime-class@1. */
+function validateRuntimeClass(value) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.runtime-class@1', value);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+module.exports = { runtimeOffer, runtimeClass, RUNTIME_CLASSES, validateRuntimeOffer, validateRuntimeClass };
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+'use strict';
+/**
+ * openvibe-sdk/storage — platform.storage-offer@1.
+ * Pure construction and validation, no I/O and no dependency at require time.
+ *
+ * const { storageOffer, validateStorageOffer } = require('openvibe-sdk/storage');
+ * const offer = storageOffer({ id, class, capacity_gb, price_per_gb_month_usd, price_per_operation_usd,
+ * region, node, durability, lifecycle_rules });
+ */
+
+/** The fields platform.storage-offer@1 requires or allows; null/undefined are stripped. */
+function storageOffer(fields) {
+ const f = fields || {};
+ const record = {
+ id: f.id,
+ class: f.class,
+ capacity_gb: f.capacity_gb,
+ price_per_gb_month_usd: f.price_per_gb_month_usd,
+ price_per_operation_usd: f.price_per_operation_usd,
+ region: f.region,
+ node: f.node,
+ durability: f.durability,
+ lifecycle_rules: f.lifecycle_rules,
+ };
+ for (const k of Object.keys(record)) if (record[k] == null) delete record[k];
+ return record;
+}
+
+/** { ok, errors } for a record against platform.storage-offer@1. */
+function validateStorageOffer(record) {
+ let contracts = null;
+ try { contracts = require('openvibe-contracts'); } catch { /* not installed */ }
+ if (!contracts) return { ok: false, errors: [] };
+ const r = contracts.validate('platform.storage-offer@1', record);
+ return { ok: Boolean(r.valid), errors: r.errors || [] };
+}
+
+module.exports = { storageOffer, validateStorageOffer };
@@ -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', './govern', './identity', './limits', './openre', './placement', './pubsub', './queue', './service', './sso', './telemetry', './testing', './usage', './valkey', './vip']);
+ assert.deepEqual(blocked.sort(), ['./cache', './cost', './db', './delivery', './events', './govern', './identity', './limits', './node', './openre', './placement', './pubsub', './queue', './runtime', './service', './sso', './storage', './telemetry', './testing', './usage', './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,78 @@
@@ -0,0 +1,78 @@
+'use strict';
+/** openvibe-sdk/cost: platform.rate-card@1 and platform.cost-snapshot@1 — build, validate, estimate. */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { run } = require('./helpers');
+const { rateCard, costSnapshot, estimate, validateRateCard, validateCostSnapshot } = require('../src/cost');
+
+const FIXTURES_RC = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.rate-card');
+const FIXTURES_CS = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.cost-snapshot');
+let contracts = null;
+try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }
+if (contracts && ![FIXTURES_RC, FIXTURES_CS].every((dir) =>
+ fs.existsSync(path.join(dir, 'valid')) && fs.existsSync(path.join(dir, 'invalid')))) contracts = null;
+
+run([
+ ['rateCard passes every valid fixture through unchanged, and they validate', async () => {
+ if (!contracts) { console.log('rate-card fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'valid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'valid', f), 'utf8'));
+ assert.deepEqual(rateCard({ ...fixture }), fixture, f);
+ assert.deepEqual(validateRateCard(fixture), { ok: true, errors: [] }, f);
+ }
+ }],
+ ['rateCard strips null/undefined and keeps zero values', async () => {
+ const r = rateCard({ id: 'rc', provider: 'p', metric: 'm', unit_size: 1, unit_price_usd: 0,
+ free_allowance: 0, reset_period: 'month', effective_from: '2026-09-01',
+ source: 'https://example.com', verified_at: '2026-09-28', adapter: null, region: undefined });
+ assert.ok(!('adapter' in r), 'null adapter stripped');
+ assert.ok(!('region' in r), 'undefined region stripped');
+ assert.equal(r.unit_price_usd, 0, 'a 0 is kept, not stripped');
+ }],
+ ['validateRateCard refuses every invalid fixture with its errors', async () => {
+ if (!contracts) { console.log('rate-card invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'invalid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'invalid', f), 'utf8'));
+ const r = validateRateCard(fixture);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+ ['costSnapshot passes every valid fixture through unchanged, and they validate', async () => {
+ if (!contracts) { console.log('cost-snapshot fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_CS, 'valid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_CS, 'valid', f), 'utf8'));
+ assert.deepEqual(costSnapshot({ ...fixture }), fixture, f);
+ assert.deepEqual(validateCostSnapshot(fixture), { ok: true, errors: [] }, f);
+ }
+ }],
+ ['costSnapshot strips null/undefined', async () => {
+ const s = costSnapshot({ window_start: '2026-09-28T00:00:00Z', window_end: '2026-09-29T00:00:00Z',
+ scope: 'events', by_target: [], actual_usd: 0, project_id: null, counterfactual_usd: undefined });
+ assert.ok(!('project_id' in s));
+ assert.ok(!('counterfactual_usd' in s));
+ }],
+ ['validateCostSnapshot refuses every invalid fixture with its errors', async () => {
+ if (!contracts) { console.log('cost-snapshot invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_CS, 'invalid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_CS, 'invalid', f), 'utf8'));
+ const r = validateCostSnapshot(fixture);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+ ['estimate: free allowance shrinks billable, unit_size prorate, cost_usd is right', async () => {
+ const card = rateCard({ id: 'rc', provider: 'p', metric: 'm', unit_size: 1000, unit_price_usd: 0.5,
+ free_allowance: 500, reset_period: 'month', effective_from: '2026-09-01',
+ source: 'https://example.com', verified_at: '2026-09-28' });
+ // 300 units: under the free allowance → billable = 0
+ assert.deepEqual(estimate(card, 300, 'ops'), { quantity: 300, unit: 'ops', billable: 0, cost_usd: 0 });
+ // 1500 units: 1000 billable, at 0.5 per 1000 → 0.5
+ const e = estimate(card, 1500, 'ops');
+ assert.equal(e.billable, 1000);
+ assert.equal(e.cost_usd, 0.5);
+ // null unit is carried through as null
+ assert.equal(estimate(card, 100).unit, null);
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+'use strict';
+/** openvibe-sdk/delivery: platform.delivery-offer@1 — build and validate. */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { run } = require('./helpers');
+const { deliveryOffer, validateDeliveryOffer } = require('../src/delivery');
+
+const FIXTURES = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.delivery-offer');
+let contracts = null;
+try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }
+if (contracts && (!fs.existsSync(path.join(FIXTURES, 'valid')) || !fs.existsSync(path.join(FIXTURES, 'invalid')))) contracts = null;
+
+run([
+ ['deliveryOffer passes every valid fixture through unchanged, and they validate', async () => {
+ if (!contracts) { console.log('delivery-offer fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES, 'valid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'valid', f), 'utf8'));
+ assert.deepEqual(deliveryOffer({ ...fixture }), fixture, f);
+ assert.deepEqual(validateDeliveryOffer(fixture), { ok: true, errors: [] }, f);
+ }
+ }],
+ ['deliveryOffer strips null/undefined', async () => {
+ const o = deliveryOffer({ id: 'd1', transports: ['http'], regions: ['us-west'], edge: true,
+ price_per_gb_usd: 0.02, price_per_request_usd: 0.00001,
+ cache_rules: { enabled: true }, node: null });
+ assert.ok(!('node' in o), 'null node stripped');
+ }],
+ ['validateDeliveryOffer refuses every invalid fixture with its errors', async () => {
+ if (!contracts) { console.log('delivery-offer invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES, 'invalid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'invalid', f), 'utf8'));
+ const r = validateDeliveryOffer(fixture);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
+'use strict';
+/** openvibe-sdk/node: platform.node-capabilities@1 — build, validate, and nodeOffers(). */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { run } = require('./helpers');
+const { nodeCapabilities, nodeOffers, validateNodeCapabilities } = require('../src/node');
+
+const FIXTURES = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.node-capabilities');
+let contracts = null;
+try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }
+if (contracts && (!fs.existsSync(path.join(FIXTURES, 'valid')) || !fs.existsSync(path.join(FIXTURES, 'invalid')))) contracts = null;
+
+run([
+ ['nodeCapabilities passes every valid fixture through unchanged, and they validate', async () => {
+ if (!contracts) { console.log('node-capabilities fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES, 'valid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'valid', f), 'utf8'));
+ assert.deepEqual(nodeCapabilities({ ...fixture }), fixture, f);
+ assert.deepEqual(validateNodeCapabilities(fixture), { ok: true, errors: [] }, f);
+ }
+ }],
+ ['nodeCapabilities strips null/undefined', async () => {
+ const c = nodeCapabilities({ node_id: 'n1', cpu: { cores: 4 }, arch: 'x86_64', memory_mb: 8192,
+ storage: { capacity_gb: 100 }, network: { ingress_mbps: 100, egress_mbps: 100 },
+ regions: ['us-west'], tags: [], costs: { per_hour_usd: 0.1 },
+ gpu: null, capabilities: undefined, agent_version: null });
+ assert.ok(!('gpu' in c), 'null gpu stripped');
+ assert.ok(!('capabilities' in c), 'undefined capabilities stripped');
+ assert.ok(!('agent_version' in c), 'null agent_version stripped');
+ }],
+ ['validateNodeCapabilities refuses every invalid fixture with its errors', async () => {
+ if (!contracts) { console.log('node-capabilities invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES, 'invalid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'invalid', f), 'utf8'));
+ const r = validateNodeCapabilities(fixture);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+ ['nodeOffers: default trust is first-party, capabilities pass through, kind is "node"', async () => {
+ const caps = nodeCapabilities({ node_id: 'n1', cpu: { cores: 8 }, arch: 'x86_64', memory_mb: 16384,
+ storage: { capacity_gb: 500, available_gb: 300 }, network: { ingress_mbps: 1000, egress_mbps: 1000 },
+ regions: ['us-west'], tags: ['gpu'], costs: { per_hour_usd: 0.2 },
+ capabilities: ['node:http', 'worker:ffmpeg'] });
+ const { resource, runtime } = nodeOffers(caps);
+ assert.equal(resource.kind, 'node');
+ assert.equal(resource.trust, 'first-party');
+ assert.equal(resource.node_id, 'n1');
+ assert.equal(resource.region, 'us-west');
+ assert.deepEqual(resource.capabilities, ['node:http', 'worker:ffmpeg']);
+ assert.equal(resource.offer_id, 'node:n1');
+ assert.equal(resource.health.status, 'up');
+ // runtime
+ assert.equal(runtime.kind, 'container');
+ assert.equal(runtime.node, 'n1');
+ assert.deepEqual(runtime.limits, { cpu_cores: 8, memory_mb: 16384 });
+ assert.equal(runtime.price.amount_usd, 0.2);
+ assert.equal(runtime.price.unit, 'hour');
+ assert.deepEqual(runtime.constraints, ['first-party']);
+ }],
+ ['nodeOffers: trust is "user-owned" only when opts.trust says so', async () => {
+ const caps = nodeCapabilities({ node_id: 'n2', cpu: { cores: 4 }, arch: 'arm64', memory_mb: 8192,
+ storage: { capacity_gb: 200 }, network: { ingress_mbps: 500, egress_mbps: 500 },
+ regions: ['eu-west'], tags: [], costs: { per_hour_usd: 0.1 } });
+ const { resource, runtime } = nodeOffers(caps, { trust: 'user-owned' });
+ assert.equal(resource.trust, 'user-owned');
+ assert.deepEqual(runtime.constraints, ['user-owned']);
+ // default (no opts) stays first-party
+ const { resource: r2 } = nodeOffers(caps);
+ assert.equal(r2.trust, 'first-party');
+ }],
+ ['nodeOffers: per-second pricing from per_hour_usd', async () => {
+ const caps = nodeCapabilities({ node_id: 'n3', cpu: { cores: 2 }, arch: 'x86_64', memory_mb: 4096,
+ storage: { capacity_gb: 100 }, network: { ingress_mbps: 100, egress_mbps: 100 },
+ regions: ['us-east'], tags: [], costs: { per_hour_usd: 0.36 } });
+ const { resource } = nodeOffers(caps);
+ assert.equal(resource.pricing.model, 'per-second');
+ assert.ok(Math.abs(resource.pricing.marginal_usd_per_unit - 0.0001) < 1e-12); // 0.36 / 3600
+ }],
+]);
@@ -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, 34);
+ assert.equal(subpaths.length, 39);
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,59 @@
@@ -0,0 +1,59 @@
+'use strict';
+/** openvibe-sdk/runtime: platform.runtime-offer@1 and platform.runtime-class@1 — build and validate. */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { run } = require('./helpers');
+const { runtimeOffer, runtimeClass, RUNTIME_CLASSES, validateRuntimeOffer, validateRuntimeClass } = require('../src/runtime');
+
+const FIXTURES_RO = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.runtime-offer');
+const FIXTURES_RC = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.runtime-class');
+let contracts = null;
+try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }
+if (contracts && ![FIXTURES_RO, FIXTURES_RC].every((dir) =>
+ fs.existsSync(path.join(dir, 'valid')) && fs.existsSync(path.join(dir, 'invalid')))) contracts = null;
+
+run([
+ ['runtimeOffer passes every valid fixture through unchanged, and they validate', async () => {
+ if (!contracts) { console.log('runtime-offer fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_RO, 'valid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RO, 'valid', f), 'utf8'));
+ assert.deepEqual(runtimeOffer({ ...fixture }), fixture, f);
+ assert.deepEqual(validateRuntimeOffer(fixture), { ok: true, errors: [] }, f);
+ }
+ }],
+ ['runtimeOffer strips null/undefined', async () => {
+ const o = runtimeOffer({ id: 'r1', kind: 'container', region: 'us-west', node: 'n1',
+ limits: { cpu_cores: 2, memory_mb: 4096 }, price: { amount_usd: 0.05, unit: 'hour' },
+ availability: null, constraints: undefined });
+ assert.ok(!('availability' in o), 'null availability stripped');
+ assert.ok(!('constraints' in o), 'undefined constraints stripped');
+ }],
+ ['validateRuntimeOffer refuses every invalid fixture with its errors', async () => {
+ if (!contracts) { console.log('runtime-offer invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_RO, 'invalid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_RO, 'invalid', f), 'utf8'));
+ const r = validateRuntimeOffer(fixture);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+ ['runtimeClass accepts every valid fixture and rejects every invalid one', async () => {
+ if (!contracts) { console.log('runtime-class fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'valid'))) {
+ const value = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'valid', f), 'utf8'));
+ assert.equal(runtimeClass(value), value, f);
+ assert.deepEqual(validateRuntimeClass(value), { ok: true, errors: [] }, f);
+ }
+ for (const f of fs.readdirSync(path.join(FIXTURES_RC, 'invalid'))) {
+ const value = JSON.parse(fs.readFileSync(path.join(FIXTURES_RC, 'invalid', f), 'utf8'));
+ assert.throws(() => runtimeClass(value), /unknown class/);
+ const r = validateRuntimeClass(value);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+ ['RUNTIME_CLASSES enumerates the known classes', async () => {
+ assert.deepEqual(RUNTIME_CLASSES, ['function', 'code', 'browser', 'linux', 'desktop', 'gpu']);
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+'use strict';
+/** openvibe-sdk/storage: platform.storage-offer@1 — build and validate. */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { run } = require('./helpers');
+const { storageOffer, validateStorageOffer } = require('../src/storage');
+
+const FIXTURES = path.join(__dirname, '..', 'node_modules', 'openvibe-contracts', 'fixtures', 'platform.storage-offer');
+let contracts = null;
+try { contracts = require('openvibe-contracts'); } catch { /* the fixture cases skip */ }
+if (contracts && (!fs.existsSync(path.join(FIXTURES, 'valid')) || !fs.existsSync(path.join(FIXTURES, 'invalid')))) contracts = null;
+
+run([
+ ['storageOffer passes every valid fixture through unchanged, and they validate', async () => {
+ if (!contracts) { console.log('storage-offer fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES, 'valid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'valid', f), 'utf8'));
+ assert.deepEqual(storageOffer({ ...fixture }), fixture, f);
+ assert.deepEqual(validateStorageOffer(fixture), { ok: true, errors: [] }, f);
+ }
+ }],
+ ['storageOffer strips null/undefined', async () => {
+ const o = storageOffer({ id: 's1', class: 'warm', capacity_gb: 1000, price_per_gb_month_usd: 0.02,
+ price_per_operation_usd: 0.00001, region: 'us-west', node: 'n1',
+ durability: { replicas: 3 }, lifecycle_rules: null });
+ assert.ok(!('lifecycle_rules' in o), 'null lifecycle_rules stripped');
+ }],
+ ['validateStorageOffer refuses every invalid fixture with its errors', async () => {
+ if (!contracts) { console.log('storage-offer invalid fixtures: skipped (matching openvibe-contracts fixtures unavailable)'); return; }
+ for (const f of fs.readdirSync(path.join(FIXTURES, 'invalid'))) {
+ const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'invalid', f), 'utf8'));
+ const r = validateStorageOffer(fixture);
+ assert.equal(r.ok, false, f);
+ assert.ok(r.errors.length > 0, f);
+ }
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+/** platform.rate-card@1 and platform.cost-snapshot@1: provider pricing and cost windows. Validated by
+ * openvibe-contracts (required lazily by the validate* functions). */
+export type RateCard = { id: string; provider: string; adapter?: string; metric: string; unit_size: number;
+ unit_price_usd: number; free_allowance: number; reset_period: 'day' | 'month' | 'none'; region?: string;
+ effective_from: string; effective_until?: string; source: string; verified_at: string };
+export type CostSnapshotTarget = { id: string; units: number; cost_usd: number; p50_ms?: number; p95_ms?: number };
+export type CostSnapshot = { window_start: string; window_end: string; project_id?: string; scope: string;
+ by_target: CostSnapshotTarget[]; actual_usd: number; counterfactual_usd?: Record<string, number> };
+export type EstimateResult = { quantity: number; unit: string | null; billable: number; cost_usd: number };
+export type ValidationError = { path: string; message: string };
+/** Null/undefined fields are stripped. */
+export declare function rateCard(fields: Partial<RateCard>): RateCard;
+export declare function costSnapshot(fields: Partial<CostSnapshot>): CostSnapshot;
+/** The cost of `quantity` units in one rate card after its free allowance. */
+export declare function estimate(card: RateCard, quantity: number, unit?: string): EstimateResult;
+export declare function validateRateCard(record: unknown): { ok: boolean; errors: ValidationError[] };
+export declare function validateCostSnapshot(record: unknown): { ok: boolean; errors: ValidationError[] };
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+/** platform.delivery-offer@1: transport and edge delivery capacity with cache policy and pricing. Validated by
+ * openvibe-contracts (required lazily by validateDeliveryOffer). */
+export type DeliveryOfferCacheRules = { enabled: boolean; ttl_seconds?: number; max_object_mb?: number };
+export type DeliveryOffer = { id: string; transports: ('http' | 'hls' | 'rtmp' | 'srt' | 'ws')[]; regions: string[];
+ edge: boolean; node?: string; price_per_gb_usd: number; price_per_request_usd: number; cache_rules: DeliveryOfferCacheRules };
+export type ValidationError = { path: string; message: string };
+/** Null/undefined fields are stripped. */
+export declare function deliveryOffer(fields: Partial<DeliveryOffer>): DeliveryOffer;
+export declare function validateDeliveryOffer(record: unknown): { ok: boolean; errors: ValidationError[] };
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+/** platform.node-capabilities@1: node hardware, placement regions, tags and operating costs. Validated by
+ * openvibe-contracts (required lazily by validateNodeCapabilities). nodeOffers() maps a descriptor to a
+ * platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1. */
+export type NodeCpu = { cores: number; threads?: number };
+export type NodeGpu = { model: string; count: number; vram_mb?: number };
+export type NodeStorage = { capacity_gb: number; available_gb?: number; kind?: string };
+export type NodeNetwork = { ingress_mbps: number; egress_mbps: number };
+export type NodeCosts = { per_hour_usd: number; egress_per_gb_usd?: number };
+export type NodeCapabilities = { node_id: string; cpu: NodeCpu; arch: string; memory_mb: number; gpu?: NodeGpu;
+ storage: NodeStorage; network: NodeNetwork; regions: string[]; tags: string[]; costs: NodeCosts;
+ capabilities?: string[]; agent_version?: string; updated_at?: string };
+export type NodeOffersOptions = { trust?: string };
+export type ValidationError = { path: string; message: string };
+/** Null/undefined fields are stripped. */
+export declare function nodeCapabilities(fields: Partial<NodeCapabilities>): NodeCapabilities;
+/** Map a Node descriptor to { resource: resource-offer@1, runtime: runtime-offer@1 }. trust defaults to
+ * "first-party"; it is "user-owned" only when opts.trust says so. */
+export declare function nodeOffers(descriptor: NodeCapabilities, opts?: NodeOffersOptions): {
+ resource: Record<string, unknown>; runtime: Record<string, unknown> };
+export declare function validateNodeCapabilities(record: unknown): { ok: boolean; errors: ValidationError[] };
@@ -24,3 +24,5 @@ export declare function signPlan(plan: RoutePlan, privateKey: any): RoutePlan;
@@ -24,3 +24,5 @@ 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 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 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;
export declare function canonical(v: unknown): string;
+/** The five trust classes (T1 ADR-046): first-party, user-owned, partner, community, external. */
+export declare const TRUST_CLASSES: readonly ['first-party', 'user-owned', 'partner', 'community', 'external'];
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+/** platform.runtime-offer@1 and platform.runtime-class@1: execution capacity and environment kinds. Validated
+ * by openvibe-contracts (required lazily by the validate* functions). */
+export type RuntimeOfferLimits = { cpu_cores: number; memory_mb: number; duration_seconds?: number; concurrency?: number };
+export type RuntimeOfferPrice = { amount_usd: number; unit: 'second' | 'hour' | 'invocation' };
+export type RuntimeOffer = { id: string; kind: 'container' | 'process' | 'wasm' | 'vm' | 'function'; region: string;
+ node: string; limits: RuntimeOfferLimits; price: RuntimeOfferPrice; availability: 'available' | 'limited' | 'unavailable';
+ constraints: string[] };
+export type RuntimeClass = 'function' | 'code' | 'browser' | 'linux' | 'desktop' | 'gpu';
+export type ValidationError = { path: string; message: string };
+/** Null/undefined fields are stripped. */
+export declare function runtimeOffer(fields: Partial<RuntimeOffer>): RuntimeOffer;
+/** Returns the class when it is a known runtime class, or throws. */
+export declare function runtimeClass(value: string): RuntimeClass;
+export declare const RUNTIME_CLASSES: RuntimeClass[];
+export declare function validateRuntimeOffer(record: unknown): { ok: boolean; errors: ValidationError[] };
+export declare function validateRuntimeClass(value: unknown): { ok: boolean; errors: ValidationError[] };
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+/** platform.storage-offer@1: durable storage capacity and operation pricing. Validated by openvibe-contracts
+ * (required lazily by validateStorageOffer). */
+export type StorageOfferDurability = { replicas: number; target_nines?: number };
+export type StorageOfferLifecycleRule = { after_days: number; action: 'transition' | 'expire'; target_class?: 'hot' | 'warm' | 'cold' };
+export type StorageOffer = { id: string; class: 'hot' | 'warm' | 'cold'; capacity_gb: number;
+ price_per_gb_month_usd: number; price_per_operation_usd: number; region: string; node: string;
+ durability: StorageOfferDurability; lifecycle_rules: StorageOfferLifecycleRule[] };
+export type ValidationError = { path: string; message: string };
+/** Null/undefined fields are stripped. */
+export declare function storageOffer(fields: Partial<StorageOffer>): StorageOffer;
+export declare function validateStorageOffer(record: unknown): { ok: boolean; errors: ValidationError[] };