openvibers/openvibe.network Commits

ADR-012 rule 5: OpenCoins are not transferable between people — the live transfer grant is revoked at every boot

OP OpenVibers committed 23 Sept 2026 · 04007829e1 · 2 files, +9 −3


Changes

ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 238edd4b..5ba71937 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -20,7 +20,6 @@ const SELF_AUDIENCE = 'openvibe.network';
const DEFAULT_GRANTS = [
['live', 'network.coins.credit', SELF_AUDIENCE, ['live']],
['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],
-- ['live', 'network.coins.transfer', SELF_AUDIENCE, ['live']],
['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
['live', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile']],
@@@ -39,6 +38,9 @@ const DEFAULT_GRANTS = [
['live', 'community.paste.moderate', 'openvibe.community', []],
];
++// Grants withdrawn by decision; applied at every boot so an old default can't come back.
++const REVOKED_GRANTS = [['live', 'network.coins.transfer', SELF_AUDIENCE]];
++
function ensureSchema(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS principal_grants (
@@@ -64,6 +66,10 @@ function ensureSchema(db) {
PRIMARY KEY (principal, route, auth, allowed, code)
);
`);
++ // ADR-012 rule 5: loyalty is not transferable between people, so nobody holds the transfer grant.
++ for (const [client, cap, aud] of REVOKED_GRANTS) {
++ db.prepare("UPDATE principal_grants SET revoked_at = CURRENT_TIMESTAMP WHERE client_id = ? AND capability = ? AND audience = ? AND revoked_at IS NULL").run(client, cap, aud);
++ }
const seed = db.prepare("INSERT OR IGNORE INTO principal_grants (client_id, capability, audience, namespaces, granted_by) VALUES (?, ?, ?, ?, 'default')");
for (const [client, cap, aud, ns] of DEFAULT_GRANTS) {
if (db.prepare('SELECT 1 FROM oauth_clients WHERE client_id = ?').get(client)) seed.run(client, cap, aud, JSON.stringify(ns));
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex 783ddeaa..701b7def 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -50,7 +50,7 @@ const server = http.createServer(app);
const claims = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.network' });
assert.ok(claims.ok, claims.reason);
assert.strictEqual(claims.claims.sub, 'svc:live');
-- assert.deepStrictEqual(claims.claims.cap, ['identity.subject.resolve', 'network.coins.credit', 'network.coins.debit', 'network.coins.transfer', 'network.modules.read', 'network.modules.write', 'network.notifications.push']);
++ assert.deepStrictEqual(claims.claims.cap, ['identity.subject.resolve', 'network.coins.credit', 'network.coins.debit', 'network.modules.read', 'network.modules.write', 'network.notifications.push']);
assert.ok(validate('identity.service-token-claims@1', claims.claims).valid);
assert.ok(claims.claims.exp - claims.claims.iat <= 300, 'short-lived');
const full = t.body.access_token;
@@@ -78,7 +78,7 @@ const server = http.createServer(app);
r = await post('/internal/coins/debit', credit({ amount: 1 }), { authorization: `Bearer ${creditOnly}` });
assert.strictEqual(r.status, 403); assert.strictEqual(r.body.code, 'capability.denied', 'narrowed token cannot debit');
r = await post('/internal/coins/transfer', { from_user_id: 7, to_user_id: 8, app_id: 'live', amount: 2, reason: 't', idempotency_key: 'tr-1' }, { authorization: `Bearer ${full}` });
-- assert.strictEqual(r.status, 200, JSON.stringify(r.body));
++ assert.strictEqual(r.status, 403, 'ADR-012: loyalty is not transferable between people'); assert.strictEqual(r.body.code, 'capability.denied');
r = await post('/internal/coins/credit', credit(), { authorization: 'Bearer not-a-jwt' });
assert.strictEqual(r.status, 401); assert.strictEqual(r.body.code, 'token.malformed');
r = await post('/internal/coins/credit', credit(), { authorization: 'Bearer not-a-jwt', 'x-internal-key': 'legacy-key' });