openvibers/openvibe.network Commits

Attempts: 1. author Claude opus done; 2. review deepseek-v4.1-flash done

OP OpenVibers committed 1 Oct 2026 · 9abdd70c63 · 7 files, +271 −1

Done by the harness job network-t2-resource-registry-slice-1-192100.

Changes

ddiff --git a/docs/t2-resource-registry.md b/docs/t2-resource-registry.md
iindex cc5fe231..271c00a4 100644
---- a/docs/t2-resource-registry.md
++++ b/docs/t2-resource-registry.md
@@@ -1,6 +1,7 @@
# T2 — the resource registry (design)
--Status: **design, ready to implement**. Nothing in this document is built yet.
++Status: **slice 1 built** (`migrations/0007_resource_registry.sql`, `server/registry/offers.js` write path,
++`test/resource-registry.test.js`); slices 2-5 are design.
Pinned versions at time of writing: `openvibe-contracts` **v0.83.0** (`package.json:30`), `openvibe-sdk` v0.25.2,
`openvibe-shared` v2.3.1. Every claim below was checked against that pin.
ddiff --git a/migrations/0007_resource_registry.sql b/migrations/0007_resource_registry.sql
nnew file mode 100644
iindex 00000000..98ab4d78
---- /dev/null
++++ b/migrations/0007_resource_registry.sql
@@@ -0,0 +1,21 @@
++-- phase: expand
++-- plan T2. The resource registry (docs/t2-resource-registry.md): what the network can place on, one
++-- platform.resource-offer@1 per row, reported by the component that owns it (server/registry/offers.js). doc is the
++-- verbatim contract document; kind, region, cell, trust, status and price_usd are copied out of it by the mapping in
++-- the design's section 2 so that filters run in SQL. kind is exactly the enum the pinned contract (Contracts 0.83.0)
++-- defines; it is widened only together with the contract (0008, slice 5).
++
++CREATE TABLE IF NOT EXISTS platform_resource_offers (
++ id text COLLATE "C" PRIMARY KEY,
++ source text COLLATE "C" NOT NULL,
++ kind text COLLATE "C" NOT NULL CHECK (kind IN ('node', 'provider')),
++ region text COLLATE "C" NOT NULL,
++ cell text COLLATE "C" NOT NULL DEFAULT 'wnam-1',
++ trust text COLLATE "C" NOT NULL CHECK (trust IN ('first-party', 'partner', 'community', 'external')),
++ status text COLLATE "C" NOT NULL CHECK (status IN ('up', 'degraded', 'down', 'draining')),
++ price_usd double precision NOT NULL DEFAULT 0,
++ doc text COLLATE "C" NOT NULL,
++ reported_at text COLLATE "C" NOT NULL
++);
++CREATE INDEX IF NOT EXISTS platform_resource_offers_source_idx ON platform_resource_offers (source, status);
++CREATE INDEX IF NOT EXISTS platform_resource_offers_kind_idx ON platform_resource_offers (kind, region, status);
ddiff --git a/node_modules b/node_modules
nnew file mode 120000
iindex 00000000..d3a68bc6
---- /dev/null
++++ b/node_modules
@@@ -0,0 +1 @@
++/home/workstation/openvibe/agents/worktrees/OpenVibe.Network-verify-main/node_modules
\\ No newline at end of file
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 8862170e..d2a412f3 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -243,6 +243,8 @@ async function seedDb(db, { log = console } = {}) {
await require('../analytics/creators').ensureSchema(db);
// The platform's machines (server/registry/nodes.js; ADR-034 §12).
await require('../registry/nodes').ensureSchema(db);
++ // What the network can place on: node and provider offers (server/registry/offers.js; plan T2).
++ await require('../registry/offers').ensureSchema(db);
// Refresh tokens: hash-only storage (server/auth/refresh-tokens.js).
await require('../auth/refresh-tokens').ensureSchema(db);
ddiff --git a/server/index.js b/server/index.js
iindex df67eb8d..47b51ae0 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -472,6 +472,12 @@ const notifyAccountData = async (userId, n) => await notificationService.create(
app.use('/api/v1/nodes', nodeRouters.pub);
app.use('/internal/nodes', nodeRouters.internal);
}
++// The resource registry (plan T2, docs/t2-resource-registry.md): node and provider offers. Slice 1 is the report;
++// network.node.report until Contracts publishes network.resource.report.
++{
++ const offerRouters = require('./registry/offers').routers({ guard: require('./identity/principals').guard('network.node.report') });
++ app.use('/internal/resources', offerRouters.internal);
++}
app.get('/internal/blocks', require('./identity/principals').guard('network.blocks.read'), require('./identity/blocks').internalHandler(db));
// The follow graph (WS-E task 4, ADR-030): public counts, a person's own follows, and who follows a target
// (its owner, or network.follows.read, service token only). Every change is network.follow.* (server/identity/follows.js).
ddiff --git a/server/registry/offers.js b/server/registry/offers.js
nnew file mode 100644
iindex 00000000..6b8476fa
---- /dev/null
++++ b/server/registry/offers.js
@@@ -0,0 +1,88 @@
++'use strict';
++/**
++ * The resource registry (plan T2, docs/t2-resource-registry.md; Contracts 0.83.0 platform.resource-offer@1): what the
++ * network can place on — every node and provider offer, with capabilities, capacity, health and pricing. The owner
++ * reports the complete set of its offers (POST /internal/resources/report, network.node.report until Contracts
++ * publishes network.resource.report); an offer absent from a later report of the same source is marked down, never
++ * deleted. This module stores; it does not plan, price or settle. Slice 1 is the write path only.
++ */
++const express = require('express');
++
++const CONTRACT = 'platform.resource-offer@1';
++const SOURCE = /^[a-z][a-z0-9-]{1,39}$/; // the same rule as network.node-report-request@1's source
++const MAX_OFFERS = 500;
++const DEFAULT_CELL = 'wnam-1';
++
++function ensureSchema(db) { /* the schema is migrations/0007_resource_registry.sql (plan T2); nothing is created at runtime */ }
++
++/** The columns copied out of a validated offer doc (design §2): the one mapping the upsert, the mark-down and the tests share. */
++function columns(doc) {
++ return {
++ id: doc.offer_id,
++ kind: doc.kind,
++ region: doc.region,
++ cell: doc.cell ?? DEFAULT_CELL,
++ trust: doc.trust,
++ status: doc.health.status,
++ price_usd: doc.pricing.marginal_usd_per_unit ?? 0,
++ };
++}
++
++/**
++ * Check a whole report before anything is written: the Network-local envelope, then every offer against the contract.
++ * → null, or the 400 body for the first problem.
++ */
++function check(body) {
++ const { validate } = require('openvibe-contracts');
++ const keys = body && typeof body === 'object' && !Array.isArray(body) ? Object.keys(body) : null;
++ if (!keys || keys.some((k) => k !== 'source' && k !== 'offers') || typeof body.source !== 'string' || !SOURCE.test(body.source)
++ || !Array.isArray(body.offers) || body.offers.length > MAX_OFFERS) {
++ return { error: `The body must be {source, offers}: source matching ${SOURCE}, at most ${MAX_OFFERS} offers` };
++ }
++ for (let i = 0; i < body.offers.length; i++) {
++ const v = validate(CONTRACT, body.offers[i]);
++ if (!v.valid) return { error: `offer ${i} does not match ${CONTRACT}`, details: (v.errors || []).slice(0, 5) };
++ }
++ return null;
++}
++
++/** Apply a checked report in one transaction: upsert every offer; mark this source's other offers down. → { offers, marked_down } */
++async function report(db, { source, offers }, now = new Date().toISOString()) {
++ ensureSchema(db);
++ const upsert = db.prepare(`INSERT INTO platform_resource_offers (id, source, kind, region, cell, trust, status, price_usd, doc, reported_at)
++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
++ ON CONFLICT(id) DO UPDATE SET source = excluded.source, kind = excluded.kind, region = excluded.region, cell = excluded.cell,
++ trust = excluded.trust, status = excluded.status, price_usd = excluded.price_usd, doc = excluded.doc, reported_at = excluded.reported_at`);
++ let marked = 0;
++ await db.tx(async () => {
++ for (const o of offers) {
++ const c = columns(o);
++ await upsert.run(c.id, source, c.kind, c.region, c.cell, c.trust, c.status, c.price_usd, JSON.stringify(o), now);
++ }
++ const ids = new Set(offers.map((o) => o.offer_id));
++ for (const row of await db.prepare('SELECT id, doc FROM platform_resource_offers WHERE source = ? AND status <> ?').all(source, 'down')) {
++ if (ids.has(row.id)) continue;
++ const doc = JSON.parse(row.doc);
++ doc.health = { status: 'down', checked_at: now };
++ doc.updated_at = now;
++ const c = columns(doc);
++ await db.prepare('UPDATE platform_resource_offers SET doc = ?, status = ? WHERE id = ?').run(JSON.stringify(doc), c.status, row.id);
++ marked++;
++ }
++ });
++ return { offers: offers.length, marked_down: marked };
++}
++
++function routers({ guard }) {
++ const internal = express.Router();
++ internal.post('/report', guard, express.json({ limit: '256kb' }), async (req, res) => {
++ const bad = check(req.body);
++ if (bad) return res.status(400).json(bad);
++ const out = await report(req.app.locals.db, req.body);
++ res.set('Cache-Control', 'no-store').set('X-Offers-Marked-Down', String(out.marked_down))
++ .json({ source: req.body.source, ...out, generated_at: new Date().toISOString() });
++ });
++ return { internal };
++}
++
++module.exports = { CONTRACT, DEFAULT_CELL, ensureSchema, columns, check, report, routers };
ddiff --git a/test/resource-registry.test.js b/test/resource-registry.test.js
nnew file mode 100644
iindex 00000000..3d1037af
---- /dev/null
++++ b/test/resource-registry.test.js
@@@ -0,0 +1,151 @@
++'use strict';
++// The resource registry, slice 1 (plan T2, docs/t2-resource-registry.md; Contracts platform.resource-offer@1):
++// POST /internal/resources/report takes Host's token only; the whole batch is validated before anything is written;
++// an offer missing from the same source's next report is marked down, not deleted; the filter columns always equal
++// the stored doc through the one mapping offers.columns().
++// node test/resource-registry.test.js
++const assert = require('assert');
++const crypto = require('crypto');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++const express = require('express');
++const { validate } = require('openvibe-contracts');
++const { getDb } = require('../server/db/database');
++const principals = require('../server/identity/principals');
++const offers = require('../server/registry/offers');
++
++(async () => {
++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-offers-'));
++const log = console.log; console.log = () => {};
++const db = getDb();
++console.log = log;
++await db.prepare("UPDATE oauth_clients SET client_secret = 'host-secret' WHERE client_id = 'host'").run();
++await db.prepare("UPDATE oauth_clients SET client_secret = 'live-secret' WHERE client_id = 'live'").run();
++
++const keys = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } });
++const app = express();
++app.use(express.urlencoded({ extended: true }));
++app.locals.db = db;
++app.locals.config = { internalKey: 'legacy-key', jwt: { issuer: 'https://openvibe.network', accessTokenExpiry: '1h' } };
++app.locals.privateKey = keys.privateKey;
++app.locals.publicKey = keys.publicKey;
++app.use('/oauth', require('../server/auth/oauth-routes'));
++const r = offers.routers({ guard: principals.guard('network.node.report', { legacy: false }) });
++app.use('/internal/resources', r.internal);
++const server = http.createServer(app);
++
++const offer = (id, extra = {}) => ({ offer_id: id, kind: 'node', node_id: id, provider: 'ovh', region: 'us-west', cell: 'wnam-1', trust: 'first-party', capabilities: ['node:http', 'events:gateway'], capacity: { cpu: { utilization: 0.2, available_cores: 6 } }, health: { status: 'up', checked_at: '2026-10-01T12:00:00Z' }, pricing: { model: 'prepaid', marginal_usd_per_unit: 0.01, unit: 'request' }, updated_at: '2026-10-01T12:00:00Z', ...extra });
++const rows = async () => db.prepare('SELECT * FROM platform_resource_offers ORDER BY id').all();
++// Design §2, invariants 1-2: every column equals the mapping of the stored doc, which is itself a contract document.
++const assertLockstep = async () => {
++ for (const row of await rows()) {
++ const doc = JSON.parse(row.doc);
++ assert.ok(validate(offers.CONTRACT, doc).valid, `${row.id}: the stored doc matches the contract`);
++ const want = offers.columns(doc);
++ for (const k of Object.keys(want)) assert.strictEqual(k === 'price_usd' ? Number(row[k]) : row[k], want[k], `${row.id}.${k} is in lockstep with doc`);
++ }
++};
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const token = async (id, secret) => (await (await fetch(`${base}/oauth/token`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: id, client_secret: secret, audience: 'openvibe.network' }) })).json()).access_token;
++ const post = (body, headers) => fetch(`${base}/internal/resources/report`, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body) })
++ .then(async (x) => ({ status: x.status, headers: x.headers, body: await x.json().catch(() => null) }));
++ try {
++ // The migration: the table exists, kind is exactly the pinned contract's enum, status the planner's.
++ const insert = (kind, status) => db.prepare("INSERT INTO platform_resource_offers (id, source, kind, region, trust, status, doc, reported_at) VALUES ('x', 'x', ?, 'us-west', 'partner', ?, '{}', 'now')").run(kind, status);
++ await assert.rejects(insert('storage', 'up'), 'kind storage has no contract at 0.83.0');
++ await assert.rejects(insert('node', 'gone'), 'status outside up|degraded|down|draining');
++ assert.deepStrictEqual(await rows(), []);
++
++ // 1. Auth matrix.
++ const auth = { authorization: `Bearer ${await token('host', 'host-secret')}` };
++ assert.strictEqual((await post({ source: 'oregon', offers: [] })).status, 403, 'nobody');
++ assert.ok([401, 403].includes((await post({ source: 'oregon', offers: [] }, { 'x-internal-key': 'legacy-key' })).status), 'not the retired shared key');
++ assert.strictEqual((await post({ source: 'oregon', offers: [] }, { authorization: `Bearer ${await token('live', 'live-secret')}` })).status, 403, 'Live lacks network.node.report');
++
++ // 2. Validation writes nothing: bad offers, a bad envelope, and a bad offer last in an otherwise good batch.
++ for (const [why, body] of [
++ ['trust outside the enum', { source: 'oregon', offers: [offer('o-1', { trust: 'random' })] }],
++ ['region missing', { source: 'oregon', offers: [(({ region, ...o }) => o)(offer('o-1'))] }],
++ ['additionalProperties: false', { source: 'oregon', offers: [offer('o-1', { address: '10.0.0.1' })] }],
++ ['kind without a contract', { source: 'oregon', offers: [offer('o-1', { kind: 'storage' })] }],
++ ['source pattern', { source: 'Oregon!', offers: [offer('o-1')] }],
++ ['extra envelope key', { source: 'oregon', offers: [offer('o-1')], cell: 'wnam-1' }],
++ ['offers not an array', { source: 'oregon', offers: offer('o-1') }],
++ ['over 500 offers', { source: 'oregon', offers: Array.from({ length: 501 }, (_, i) => ({ offer_id: `n${i}` })) }],
++ ]) {
++ const x = await post(body, auth);
++ assert.strictEqual(x.status, 400, why);
++ assert.ok(typeof x.body.error === 'string', why);
++ }
++ assert.deepStrictEqual(await rows(), [], 'no bad report wrote anything');
++
++ let x = await post({ source: 'oregon', offers: [offer('o-1')] }, auth);
++ assert.strictEqual(x.status, 200, JSON.stringify(x.body));
++ assert.deepStrictEqual([x.body.source, x.body.offers, x.body.marked_down], ['oregon', 1, 0]);
++ assert.strictEqual(x.headers.get('x-offers-marked-down'), '0');
++ const before = await rows();
++ x = await post({ source: 'oregon', offers: [offer('o-2'), offer('o-3'), offer('o-4', { pricing: { model: 'free', marginal_usd_per_unit: 0 } })] }, auth);
++ assert.strictEqual(x.status, 400);
++ assert.strictEqual(x.body.error, 'offer 2 does not match platform.resource-offer@1');
++ assert.ok(Array.isArray(x.body.details) && x.body.details.length > 0);
++ assert.deepStrictEqual(await rows(), before, 'a bad offer last: no upsert of the good ones, o-1 not marked down');
++
++ // 3. Upsert. One offer without cell and one without a marginal price, so both defaulted columns are covered.
++ x = await post({ source: 'oregon', offers: [
++ offer('o-1'),
++ offer('o-2', { kind: 'provider', node_id: undefined, adapter: 'nats-v1', trust: 'partner', region: 'us-east', cell: undefined }),
++ offer('o-3', { trust: 'community', pricing: { model: 'free-allowance' }, health: { status: 'draining' } }),
++ ] }, auth);
++ assert.strictEqual(x.status, 200, JSON.stringify(x.body));
++ assert.strictEqual(x.body.offers, 3);
++ let all = await rows();
++ assert.deepStrictEqual(all.map((o) => [o.id, o.source, o.kind, o.cell, o.status, Number(o.price_usd)]),
++ [['o-1', 'oregon', 'node', 'wnam-1', 'up', 0.01], ['o-2', 'oregon', 'provider', 'wnam-1', 'up', 0.01], ['o-3', 'oregon', 'node', 'wnam-1', 'draining', 0]]);
++ assert.strictEqual(JSON.parse(all[1].doc).cell, undefined, 'the doc is stored as reported: the default lives in the column only');
++ await assertLockstep();
++
++ x = await post({ source: 'oregon', offers: [offer('o-1', { pricing: { model: 'per-request', marginal_usd_per_unit: 0.2, unit: 'request' } }), offer('o-2', { kind: 'provider', node_id: undefined, trust: 'partner', region: 'us-east', cell: undefined }), offer('o-3', { pricing: { model: 'free-allowance' }, health: { status: 'draining' } })] }, auth);
++ assert.strictEqual(x.body.marked_down, 0);
++ const o1 = (await rows()).find((o) => o.id === 'o-1');
++ assert.strictEqual(Number(o1.price_usd), 0.2, 'the column follows a re-report');
++ assert.strictEqual(JSON.parse(o1.doc).pricing.marginal_usd_per_unit, 0.2, 'and so does the doc');
++ await assertLockstep();
++
++ // 4. Mark-down: o-2 leaves oregon's report; frankfurt's offers are untouched; nothing is deleted.
++ x = await post({ source: 'frankfurt', offers: [offer('f-1', { region: 'eu-central', cell: 'weur-1' })] }, auth);
++ assert.strictEqual(x.headers.get('x-offers-marked-down'), '0', 'a new source marks nothing of another source down');
++ const now = Date.now();
++ x = await post({ source: 'oregon', offers: [offer('o-1'), offer('o-3', { health: { status: 'draining' } })] }, auth);
++ assert.strictEqual(x.status, 200);
++ assert.strictEqual(x.headers.get('x-offers-marked-down'), '1');
++ all = await rows();
++ assert.deepStrictEqual(all.map((o) => [o.id, o.source, o.status]), [['f-1', 'frankfurt', 'up'], ['o-1', 'oregon', 'up'], ['o-2', 'oregon', 'down'], ['o-3', 'oregon', 'draining']]);
++ const o2 = JSON.parse(all.find((o) => o.id === 'o-2').doc);
++ assert.strictEqual(o2.health.status, 'down');
++ assert.ok(Date.parse(o2.health.checked_at) >= now - 1000, 'checked_at is the mark-down time');
++ await assertLockstep();
++ x = await post({ source: 'oregon', offers: [offer('o-1'), offer('o-3', { health: { status: 'draining' } })] }, auth);
++ assert.strictEqual(x.headers.get('x-offers-marked-down'), '0', 'a down offer is not marked down again');
++
++ // An empty report from a source marks all of its remaining offers down (draining included).
++ x = await post({ source: 'oregon', offers: [] }, auth);
++ assert.strictEqual(x.headers.get('x-offers-marked-down'), '2');
++ assert.deepStrictEqual((await rows()).map((o) => o.status), ['up', 'down', 'down', 'down']);
++ await assertLockstep();
++
++ // A down offer comes back up when its source reports it again.
++ await post({ source: 'oregon', offers: [offer('o-2', { kind: 'provider', node_id: 'o-2' })] }, auth);
++ assert.strictEqual((await rows()).find((o) => o.id === 'o-2').status, 'up');
++ await assertLockstep();
++ console.log('resource-registry: all tests passed');
++ } finally {
++ server.close();
++ fs.rmSync(dir, { recursive: true, force: true });
++ }
++})().catch((err) => { console.error(err); process.exit(1); });
++})().catch(err => { console.error(err); process.exit(1); });