openvibers/openvibe.sdk Commits

Fix new SDK subpath tests and document Fabric exports (#8)

OP OpenVibers committed 4 Oct 2026 · 9b63449d2e · 32 files, +770 −10

checks run by GitHub CI (test) · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.SDK/pull/8

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex c1a587c..4bd6fa1 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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` and
every optional dimension pass through, `at` defaults to now (ISO) and `source` to `openvibe-sdk/usage`, null fields
ddiff --git a/README.md b/README.md
iindex 59b6068..11221e5 100644
---- a/README.md
++++ b/README.md
@@@ -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 |
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex a2aa107..f2d2331 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -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,
ddiff --git a/esm/cost.mjs b/esm/cost.mjs
nnew file mode 100644
iindex 0000000..155c077
---- /dev/null
++++ b/esm/cost.mjs
@@@ -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;
ddiff --git a/esm/delivery.mjs b/esm/delivery.mjs
nnew file mode 100644
iindex 0000000..6dc927a
---- /dev/null
++++ b/esm/delivery.mjs
@@@ -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;
ddiff --git a/esm/node.mjs b/esm/node.mjs
nnew file mode 100644
iindex 0000000..fd37bf9
---- /dev/null
++++ b/esm/node.mjs
@@@ -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;
ddiff --git a/esm/placement.mjs b/esm/placement.mjs
iindex a787282..58a9e44 100644
---- a/esm/placement.mjs
++++ b/esm/placement.mjs
@@@ -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;
ddiff --git a/esm/runtime.mjs b/esm/runtime.mjs
nnew file mode 100644
iindex 0000000..ab42d9e
---- /dev/null
++++ b/esm/runtime.mjs
@@@ -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;
ddiff --git a/esm/storage.mjs b/esm/storage.mjs
nnew file mode 100644
iindex 0000000..8658ae5
---- /dev/null
++++ b/esm/storage.mjs
@@@ -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;
ddiff --git a/package-lock.json b/package-lock.json
iindex cf272c4..0cdc14d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex 23e6aeb..99fa847 100644
---- a/package.json
++++ b/package.json
@@@ -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,
ddiff --git a/scripts/esm.js b/scripts/esm.js
iindex b89e2a0..3dd0209 100644
---- a/scripts/esm.js
++++ b/scripts/esm.js
@@@ -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',
ddiff --git a/src/core/index.js b/src/core/index.js
iindex 7214565..0d982bc 100644
---- a/src/core/index.js
++++ b/src/core/index.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,
ddiff --git a/src/cost.js b/src/cost.js
nnew file mode 100644
iindex 0000000..c38cd4a
---- /dev/null
++++ b/src/cost.js
@@@ -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 };
ddiff --git a/src/delivery.js b/src/delivery.js
nnew file mode 100644
iindex 0000000..85c232a
---- /dev/null
++++ b/src/delivery.js
@@@ -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 };
ddiff --git a/src/node.js b/src/node.js
nnew file mode 100644
iindex 0000000..3de3944
---- /dev/null
++++ b/src/node.js
@@@ -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 };
ddiff --git a/src/placement.js b/src/placement.js
iindex de68db6..bfc7485 100644
---- a/src/placement.js
++++ b/src/placement.js
@@@ -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 };
ddiff --git a/src/runtime.js b/src/runtime.js
nnew file mode 100644
iindex 0000000..ea7ace9
---- /dev/null
++++ b/src/runtime.js
@@@ -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 };
ddiff --git a/src/storage.js b/src/storage.js
nnew file mode 100644
iindex 0000000..6e9d226
---- /dev/null
++++ b/src/storage.js
@@@ -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 };
ddiff --git a/test/browser-entries.test.js b/test/browser-entries.test.js
iindex 4bc6e7a..58fd979 100644
---- a/test/browser-entries.test.js
++++ b/test/browser-entries.test.js
@@@ -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`);
ddiff --git a/test/cost.test.js b/test/cost.test.js
nnew file mode 100644
iindex 0000000..d3a84f2
---- /dev/null
++++ b/test/cost.test.js
@@@ -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);
++ }],
++]);
ddiff --git a/test/delivery.test.js b/test/delivery.test.js
nnew file mode 100644
iindex 0000000..95b61c3
---- /dev/null
++++ b/test/delivery.test.js
@@@ -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);
++ }
++ }],
++]);
ddiff --git a/test/node.test.js b/test/node.test.js
nnew file mode 100644
iindex 0000000..17d8d6f
---- /dev/null
++++ b/test/node.test.js
@@@ -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
++ }],
++]);
ddiff --git a/test/pack.test.js b/test/pack.test.js
iindex d4c248c..c12794d 100644
---- a/test/pack.test.js
++++ b/test/pack.test.js
@@@ -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 });
ddiff --git a/test/runtime.test.js b/test/runtime.test.js
nnew file mode 100644
iindex 0000000..21da576
---- /dev/null
++++ b/test/runtime.test.js
@@@ -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']);
++ }],
++]);
ddiff --git a/test/storage.test.js b/test/storage.test.js
nnew file mode 100644
iindex 0000000..936c85f
---- /dev/null
++++ b/test/storage.test.js
@@@ -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);
++ }
++ }],
++]);
ddiff --git a/types/cost.d.ts b/types/cost.d.ts
nnew file mode 100644
iindex 0000000..5e1843b
---- /dev/null
++++ b/types/cost.d.ts
@@@ -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[] };
ddiff --git a/types/delivery.d.ts b/types/delivery.d.ts
nnew file mode 100644
iindex 0000000..e9a08de
---- /dev/null
++++ b/types/delivery.d.ts
@@@ -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[] };
ddiff --git a/types/node.d.ts b/types/node.d.ts
nnew file mode 100644
iindex 0000000..657e48f
---- /dev/null
++++ b/types/node.d.ts
@@@ -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[] };
ddiff --git a/types/placement.d.ts b/types/placement.d.ts
iindex 7e5e909..660730e 100644
---- a/types/placement.d.ts
++++ b/types/placement.d.ts
@@@ -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'];
ddiff --git a/types/runtime.d.ts b/types/runtime.d.ts
nnew file mode 100644
iindex 0000000..b33bb6f
---- /dev/null
++++ b/types/runtime.d.ts
@@@ -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[] };
ddiff --git a/types/storage.d.ts b/types/storage.d.ts
nnew file mode 100644
iindex 0000000..fe11297
---- /dev/null
++++ b/types/storage.d.ts
@@@ -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[] };