openvibers/openvibe.live Commits

Service tokens to Network on link-account, identity legacy-map and resolve-batch (daily identity sync), coins/stats and resolve-anon, which Network guards by capability since 2026-09-24; a refused token falls back to the internal key once (compatibility register C-50..C-57)

OP OpenVibers committed 24 Sept 2026 · 680d38325a · 4 files, +25 −16


Changes

ddiff --git a/server/auth/identity-sync.js b/server/auth/identity-sync.js
iindex a9d2ebca..be9cf5d8 100644
---- a/server/auth/identity-sync.js
++++ b/server/auth/identity-sync.js
@@@ -38,6 +38,22 @@ function subjectOf(liveUserId) {
return row ? row.subject_id || null : null;
}
++/** POST to Network with a service token where it accepts one (else the internal key); a refused token falls back once. */
++async function networkPost(path, body) {
++ const principal = require('../net/network-principal');
++ for (let attempt = 0; attempt < 2; attempt++) {
++ const auth = await principal.headersFor(path);
++ const res = await fetch(`${OV_NETWORK_INTERNAL_URL}${path}`, {
++ method: 'POST',
++ headers: { 'Content-Type': 'application/json', ...auth },
++ body: JSON.stringify(body),
++ signal: AbortSignal.timeout(15_000),
++ });
++ if (res.status === 401 && auth.Authorization && attempt === 0) { principal.tokenRejected('401'); continue; }
++ return res;
++ }
++}
++
async function syncLegacyMap() {
if (!INTERNAL_API_KEY) return { skipped: 'no INTERNAL_API_KEY' };
const rows = db.getDb().prepare("SELECT user_id, service_user_id FROM linked_accounts WHERE service = 'network' AND service_user_id GLOB '[0-9]*' ORDER BY user_id").all();
@@@ -46,12 +62,7 @@ async function syncLegacyMap() {
const entries = rows.slice(i, i + BATCH).map(r => ({
network_user_id: Number(r.service_user_id), source_system: 'live', source_type: 'user', source_id: String(r.user_id), verified: true,
}));
-- const res = await fetch(`${OV_NETWORK_INTERNAL_URL}/internal/identity/legacy-map`, {
-- method: 'POST',
-- headers: { 'Content-Type': 'application/json', 'X-Internal-Key': INTERNAL_API_KEY },
-- body: JSON.stringify({ entries }),
-- signal: AbortSignal.timeout(15_000),
-- });
++ const res = await networkPost('/internal/identity/legacy-map', { entries });
if (res.status === 404) return { skipped: 'network has no /internal/identity yet' };
if (!res.ok) throw new Error(`legacy-map ${res.status}`);
const out = await res.json();
@@@ -70,12 +81,7 @@ async function backfillSubjects() {
const store = db.getDb().prepare("UPDATE linked_accounts SET subject_id = ? WHERE service = 'network' AND user_id = ? AND service_user_id = ? AND subject_id IS NULL");
for (let i = 0; i < rows.length; i += BATCH) {
const batch = rows.slice(i, i + BATCH);
-- const res = await fetch(`${OV_NETWORK_INTERNAL_URL}/internal/identity/resolve-batch`, {
-- method: 'POST',
-- headers: { 'Content-Type': 'application/json', 'X-Internal-Key': INTERNAL_API_KEY },
-- body: JSON.stringify({ system: 'network', type: 'user', ids: batch.map(r => String(r.service_user_id)) }),
-- signal: AbortSignal.timeout(15_000),
-- });
++ const res = await networkPost('/internal/identity/resolve-batch', { system: 'network', type: 'user', ids: batch.map(r => String(r.service_user_id)) });
if (res.status === 404) return { ...total, skipped: 'network has no /internal/identity yet' };
if (!res.ok) throw new Error(`resolve-batch ${res.status}`);
const { results = {} } = await res.json();
ddiff --git a/server/monetization/wallet-client.js b/server/monetization/wallet-client.js
iindex 7575f567..d8dafec4 100644
---- a/server/monetization/wallet-client.js
++++ b/server/monetization/wallet-client.js
@@@ -149,7 +149,7 @@ async function networkCoinStats() {
try {
const ctl = AbortSignal.timeout ? AbortSignal.timeout(2500) : undefined;
const res = await fetch(`${NETWORK_INTERNAL_URL}/internal/coins/stats`, {
-- headers: { 'X-Internal-Key': INTERNAL_API_KEY, Accept: 'application/json' },
++ headers: { ...(await principal.headersFor('/internal/coins/stats')), Accept: 'application/json' },
signal: ctl,
});
if (!res.ok) return _statsCache.data;
ddiff --git a/server/net/network-principal.js b/server/net/network-principal.js
iindex 3c4a2a67..24647bef 100644
---- a/server/net/network-principal.js
++++ b/server/net/network-principal.js
@@@ -18,7 +18,9 @@ const CLIENT_SECRET = process.env.OV_OAUTH_CLIENT_SECRET || '';
const PAUSE_MS = 5 * 60 * 1000;
// Network routes that accept a service token (OpenVibe.Network server/internal/routes.js TOKEN_ROUTES).
--const TOKEN_PATHS = new Set(['/internal/coins/credit', '/internal/coins/debit', '/internal/coins/transfer', '/internal/notifications/push', '/internal/notifications/push-bulk', '/internal/events/stream-live']);
++const TOKEN_PATHS = new Set(['/internal/coins/credit', '/internal/coins/debit', '/internal/coins/transfer', '/internal/notifications/push', '/internal/notifications/push-bulk', '/internal/events/stream-live',
++ // Network accepts a service token on these since 2026-09-24 (identity.subject.resolve / network.coins.credit).
++ '/internal/link-account', '/internal/identity/legacy-map', '/internal/identity/resolve-batch', '/internal/coins/stats', '/internal/resolve-anon']);
const _clients = new Map();
/** One cached client-credentials token client per audience (openvibe.network, openvibe.community, ...). */
ddiff --git a/test/network-principal.test.js b/test/network-principal.test.js
iindex 8fed15fa..19ecc1c6 100644
---- a/test/network-principal.test.js
++++ b/test/network-principal.test.js
@@@ -64,10 +64,11 @@ const network = http.createServer((req, res) => {
assert.deepStrictEqual(seen.map((s) => s.auth), ['token', 'token']);
assert.strictEqual(tokenCalls, 1, 'one token for both calls');
-- // 2. Routes Network doesn't guard by capability keep the key.
++ // 2. link-account takes a service token too since Network guards it (identity.subject.resolve, 2026-09-24).
seen.length = 0;
await (async () => { notify.reportLinkedAccount({ id: uid, username: 'p' }); await new Promise((x) => setTimeout(x, 150)); })();
-- assert.deepStrictEqual(seen.map((s) => `${s.url}:${s.auth}`), ['/internal/link-account:key']);
++ assert.deepStrictEqual(seen.map((s) => `${s.url}:${s.auth}`), ['/internal/link-account:token']);
++ assert.ok(!principal.TOKEN_PATHS.has('/internal/notifications/mark-read'), 'routes Network does not guard by capability keep the key');
// 3. A refused token is retried once with the key, and the key is used until the pause ends.
seen.length = 0; mode.coins = 'refuse';