openvibers/openvibe.live Commits

X-Internal-Key is gone from Live (plan T2, step 3 of the sweep): every internal route takes only a Network service token with the capability it performs (live.avatar.write, live.url_registry.refresh, live.analytics.read, live.chat_mirror.write for Chat's staged-table handoff), loopback only. server/net/internal-key.js and config.internalApiKey are deleted; the proxy rule lives in service-guard.js. Network already sends its token on every call to Live (79a4eea). Tests: the retired key opens nothing (alone or beside a bad token), a token through the public edge is refused, and no server file reads or sends the key. SETUP, README, .env.example and the API docs describe the service token. npm test 151/152, 1 skipped.

OP OpenVibers committed 29 Sept 2026 · 66f590b7e5 · 11 files, +43 −96


Changes

ddiff --git a/.env.example b/.env.example
iindex b8ef36f4..7e69dfa8 100644
---- a/.env.example
++++ b/.env.example
@@@ -55,8 +55,6 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
# OAuth client credentials (client id `live` is seeded in the Network DB)
OV_OAUTH_CLIENT_ID=live
OV_OAUTH_CLIENT_SECRET=
--# Per-service key for Network's /internal/* endpoints (header X-Internal-Key)
--INTERNAL_API_KEY=change-me-to-a-random-secret
# OpenVibe.Events (durable events): stream lifecycle is published through Live's outbox with the
# OAuth client above (grant events.event.publish). Unset = off. EVENTS_PUBLISH=off also disables it.
#EVENTS_URL=http://127.0.0.1:4300
ddiff --git a/README.md b/README.md
iindex f01d8e85..7eca99da 100644
---- a/README.md
++++ b/README.md
@@@ -142,7 +142,7 @@ npm run init-db
npm run dev
```
--Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL_URL`, `INTERNAL_API_KEY`, `OV_NETWORK_PUBLIC_KEY`, `MEDIA_URL`, `MEDIA_PUBLIC_URL`, `MEDIA_API_KEY`, `MEDIA_WEBHOOK_SECRET`.
++Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL_URL`, `OV_OAUTH_CLIENT_ID`, `OV_OAUTH_CLIENT_SECRET`, `OV_NETWORK_PUBLIC_KEY`, `MEDIA_URL`, `MEDIA_PUBLIC_URL`, `MEDIA_API_KEY`, `MEDIA_WEBHOOK_SECRET`.
---
ddiff --git a/SETUP.md b/SETUP.md
iindex 8f598ab5..96a96204 100644
---- a/SETUP.md
++++ b/SETUP.md
@@@ -6,7 +6,7 @@ This document describes the OpenVibe.Live runtime setup, local development, and
OpenVibe.Live (port **3000**) is the streaming front — ingest (RTMP / WHIP / WebRTC / JSMPEG), chat, channels, monetization (Vibes), moderation, and the SPA. Two sibling services do heavy lifting:
--- **OpenVibe.Network** (`openvibe.network`, port **4000**) — SSO/OAuth2 identity provider (client id `live`), RS256 JWTs, URL registry, notifications, and the network-wide **OpenCoins wallet**. Live calls the wallet server-to-server via `OV_NETWORK_INTERNAL_URL` with `X-Internal-Key`.
++- **OpenVibe.Network** (`openvibe.network`, port **4000**) — SSO/OAuth2 identity provider (client id `live`), RS256 JWTs, URL registry, notifications, and the network-wide **OpenCoins wallet**. Live calls the wallet server-to-server via `OV_NETWORK_INTERNAL_URL` with its service token (client credentials).
- **OpenVibe.Media** (`openvibe.media`, port **4100**) — owns **VODs, clips, pastes, thumbnails, and file storage** (Media API v1). Live records streams by pointing Media at its ingest (RTMP pull / RTP ports 12000-12199), proxies the SPA's `/api/vods`, `/api/clips`, `/api/pastes`, `/api/thumbnails` calls to it (`server/media-client.js`), 302-redirects file payloads to `MEDIA_PUBLIC_URL`, and receives `vod.ready` / `clip.ready` webhooks at `POST /internal/media-webhook`.
Live keeps locally: live.db (users/streams/chat/channel state), Vibes (PayPal tipping/cashout), channel points, comments, stream memories + AI state for Media-hosted vods/clips (`vod_ai_state` / `clip_ai_state`), ephemeral live thumbnails, and the song-request queue.
@@@ -43,8 +43,7 @@ cp .env.example .env
- `JWT_SECRET` — app-specific secret for Live's own JWT operations.
- `OV_NETWORK_URL` — public SSO issuer (default `https://openvibe.network`).
- `OV_NETWORK_INTERNAL_URL` — internal Network base (default `http://127.0.0.1:4000`).
--- `INTERNAL_API_KEY` — key for Network's `/internal/*` endpoints (header `X-Internal-Key`).
--- `OV_OAUTH_CLIENT_ID` / `OV_OAUTH_CLIENT_SECRET` — OAuth client (`live`).
++- `OV_OAUTH_CLIENT_ID` / `OV_OAUTH_CLIENT_SECRET` — OAuth client (`live`). The same client mints Live's service tokens for Network's `/internal/*` endpoints (client credentials; the shared `INTERNAL_API_KEY` is retired).
- `OV_NETWORK_PUBLIC_KEY` — path to the Network RS256 public key (offline JWT verification).
- `MEDIA_URL` — internal Media API base (default `http://127.0.0.1:4100`).
- `MEDIA_PUBLIC_URL` — public Media host (default `https://openvibe.media`).
@@@ -94,7 +93,6 @@ NODE_ENV=development
BASE_URL=http://localhost:3000
OV_NETWORK_URL=http://localhost:4000
OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
--INTERNAL_API_KEY=your-shared-secret
OV_NETWORK_PUBLIC_KEY=./data/keys/openvibe-network-public.pem
MEDIA_URL=http://127.0.0.1:4100
MEDIA_PUBLIC_URL=http://127.0.0.1:4100
@@@ -111,7 +109,7 @@ Network's `local-dev` bootstrap seeds the `live` OAuth client with `http://local
On startup, the server:
--- refreshes URL registry values from OpenVibe.Network (needs `OV_NETWORK_INTERNAL_URL` + `INTERNAL_API_KEY`).
++- refreshes URL registry values from OpenVibe.Network (needs `OV_NETWORK_INTERNAL_URL` + `OV_OAUTH_CLIENT_SECRET`).
- initializes the database and schema.
- seeds/refreshes built-in themes (default theme slug: `vibe`).
- creates an admin user from `ADMIN_USERNAME` / `ADMIN_PASSWORD` if no admin exists (dev defaults `admin` / `changeme123`).
@@@ -160,7 +158,7 @@ Completion arrives via the `vod.ready` / `clip.ready` webhooks.
- **Invalid redirect_uri**: local callback URIs not registered on the `live` OAuth client in Network.
- **VOD/clip/paste endpoints return 502**: OpenVibe.Media is down or `MEDIA_URL`/`MEDIA_API_KEY` are wrong.
- **Webhook 401s in Media logs**: `MEDIA_WEBHOOK_SECRET` mismatch.
--- **OpenCoins balance always 0 / spends fail**: `OV_NETWORK_INTERNAL_URL`/`INTERNAL_API_KEY` wrong, or the user has no linked Network account.
++- **OpenCoins balance always 0 / spends fail**: `OV_NETWORK_INTERNAL_URL`/`OV_OAUTH_CLIENT_SECRET` wrong, or the user has no linked Network account.
- **CORS rejects browser traffic**: `BASE_URL` set to localhost in production.
- **WebRTC fails**: `mediasoup` could not initialize or TURN is not configured.
- **RTMP fails to start**: port conflict on `RTMP_PORT`.
ddiff --git a/public/fragments/documentation.html b/public/fragments/documentation.html
iindex 6e74ae99..7b3c737a 100644
---- a/public/fragments/documentation.html
++++ b/public/fragments/documentation.html
@@@ -156,7 +156,7 @@
<pre><code>openvibe.network /api/admin/streamer/* → OpenVibe.Live /api/admin/*
openvibe.network /api/admin/streamer-tts/* → OpenVibe.Live /api/tts/*
openvibe.network /api/admin/streamer-mod/* → OpenVibe.Live /api/mod/*</code></pre>
--<p>Protected by X-Internal-Key header for server-to-server calls.</p>
++<p>Server-to-server calls carry a short-lived service token from openvibe.network, scoped to the one capability each route performs.</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
@@@ -1073,7 +1073,7 @@ https://www.101soundboards.com/sounds/12345 150p 2</code></pre>
</table>
<h3>Internal Events API (server-to-server)</h3>
--<p>OpenVibe.Live emits events to openvibe.network via internal API. Protected by X-Internal-Key header.</p>
++<p>OpenVibe.Live emits events to openvibe.network through its internal API, with a service token scoped to the capability of each call.</p>
<table class="doc-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
ddiff --git a/server/config.js b/server/config.js
iindex 026e3323..c013b902 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -160,7 +160,6 @@ function buildConfig(registryValues) {
baseUrl,
openvibeToolsUrl, // public-facing URL of the SSO provider (e.g. https://openvibe.network)
nodeEnv: process.env.NODE_ENV || 'development',
-- internalApiKey: process.env.INTERNAL_API_KEY || process.env.OV_INTERNAL_KEY || '',
webrtc: {
publicUrl: webRtcPublicUrl,
},
ddiff --git a/server/index.js b/server/index.js
iindex ae699e00..21ee522e 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -607,7 +607,7 @@ app.post('/internal/openre-events', require('./openre/mirror').webhookHandler);
// Internal (server-to-server) routes — allow openvibe.network to call into this service
// Summary numbers for the Network's navigation service (ordering sites by real use).
// A service token with live.analytics.read (loopback only); returns totals, never rows.
--app.get('/internal/analytics-summary', require('./net/service-guard').guardOrKey('live.analytics.read'), (req, res) => {
++app.get('/internal/analytics-summary', require('./net/service-guard').guard('live.analytics.read'), (req, res) => {
try {
const days = Math.min(parseInt(req.query.days, 10) || 7, 90);
const st = analytics.getStats({ days }) || {};
ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex a38a464f..5cdd60cd 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -5,12 +5,11 @@ const router = express.Router();
const config = require('../config');
const db = require('../db/database');
--const { guardOrKey } = require('../net/service-guard');
++const { guard } = require('../net/service-guard');
// Service-to-service only, loopback only (nothing that came through nginx): each route checks the one capability it
--// performs on a Network service token (server/net/service-guard.js). The X-Internal-Key path is kept only while its
--// callers move to tokens (plan T2).
--const chatTablesGuard = guardOrKey('live.chat_mirror.write'); // OpenVibe.Chat's staged-table handoff (T3 deletes)
++// performs on a Network service token (server/net/service-guard.js); nothing else gets in (X-Internal-Key: plan T2).
++const chatTablesGuard = guard('live.chat_mirror.write'); // OpenVibe.Chat's staged-table handoff (T3 deletes)
// CHAT_AUTHORITY=chat: OpenVibe.Chat caches users; a role or avatar pushed here reaches it at once.
function notifyChat(userId) {
@@@ -55,7 +54,7 @@ router.post('/chat-tables/:table', chatTablesGuard, async (req, res) => {
// Footer site copy is written by OpenVibe.AI for the Network directly (network.site_copy); the
// /internal/ai/site-copy fallback that used to live here was retired on 2026-09-23.
--router.post('/url-registry/refresh', guardOrKey('live.url_registry.refresh'), async (req, res) => {
++router.post('/url-registry/refresh', guard('live.url_registry.refresh'), async (req, res) => {
try {
await config.refreshRegistry();
console.log('[Internal] URL registry refresh requested');
@@@ -73,7 +72,7 @@ router.post('/url-registry/refresh', guardOrKey('live.url_registry.refresh'), as
// ── The account's avatar changed on the Network (or on another site) ─────────
// The avatar is a network-wide property (OpenVibe.Network server/profile/avatar.js). Live keeps a copy on its
// own user row because every stream card, chat line and profile reads it locally.
--router.post('/user-avatar', guardOrKey('live.avatar.write'), (req, res) => {
++router.post('/user-avatar', guard('live.avatar.write'), (req, res) => {
try {
const { username, openvibenetwork_id, avatar_url } = req.body || {};
let url = null;
ddiff --git a/server/net/internal-key.js b/server/net/internal-key.js
ddeleted file mode 100644
iindex fcddf624..00000000
---- a/server/net/internal-key.js
++++ /dev/null
@@@ -1,33 +0,0 @@
--'use strict';
--/**
-- * The trust rule for Live's internal (server-to-server) routes, in one place.
-- *
-- * viaProxy(req) the request came through nginx or Cloudflare: nginx adds X-Forwarded-For /
-- * X-Real-IP to everything from outside (Cloudflare adds CF-Connecting-IP), and
-- * loopback callers never send them. Internal routes refuse such a request.
-- * internalKeyMatches(k) timing-safe comparison with INTERNAL_API_KEY (both sides hashed first, so
-- * neither the content nor the length leaks through timing).
-- * internalKeyOk(req) both: loopback and the right X-Internal-Key. Only service-guard.js guardOrKey
-- * uses it, for callers still moving to service tokens (plan T2 deletes it).
-- */
--const crypto = require('crypto');
--
--function viaProxy(req) {
-- const h = (req && req.headers) || {};
-- return !!(h['x-forwarded-for'] || h['x-real-ip'] || h['cf-connecting-ip']);
--}
--
--const digest = (v) => crypto.createHash('sha256').update(String(v)).digest();
--
--function internalKeyMatches(given) {
-- const key = String(require('../config').internalApiKey || '');
-- const presented = String(given || '');
-- if (!key || !presented) return false;
-- return crypto.timingSafeEqual(digest(presented), digest(key));
--}
--
--function internalKeyOk(req) {
-- return !viaProxy(req) && internalKeyMatches(req && req.headers && req.headers['x-internal-key']);
--}
--
--module.exports = { viaProxy, internalKeyMatches, internalKeyOk };
ddiff --git a/server/net/service-guard.js b/server/net/service-guard.js
iindex 56fb69d8..a358876d 100644
---- a/server/net/service-guard.js
++++ b/server/net/service-guard.js
@@@ -9,12 +9,18 @@
*
* The capability ids are registered in openvibe-contracts (checked with capabilities.check()); an
* id the pinned registry does not know yet is checked as a plain grant with capabilities.grants()
-- * until the pin moves. Like X-Internal-Key routes, these are loopback-only: anything that came
-- * through nginx is refused.
++ * until the pin moves. Loopback only: anything that came through nginx is refused. X-Internal-Key is
++ * gone (plan T2): every internal route is guard(capability).
*/
const { serviceAuth, capabilities, http } = require('openvibe-contracts');
const { getNetworkPublicKey, getNetworkIssuer } = require('../auth/auth');
--const { viaProxy } = require('./internal-key');
++
++/** The request came through nginx or Cloudflare: nginx adds X-Forwarded-For / X-Real-IP to everything from outside
++ * (Cloudflare adds CF-Connecting-IP), and loopback callers never send them. Internal routes refuse such a request. */
++function viaProxy(req) {
++ const h = (req && req.headers) || {};
++ return !!(h['x-forwarded-for'] || h['x-real-ip'] || h['cf-connecting-ip']);
++}
const AUDIENCE = 'openvibe.live';
@@@ -38,18 +44,4 @@ function guard(capability) {
};
}
--/**
-- * While X-Internal-Key is retired (plan T2): a request that presents a Bearer is judged on the token alone (never
-- * downgraded to the key); one without a Bearer still passes with the key from loopback. Callers move to tokens, then
-- * every route becomes guard(capability) and the key goes.
-- */
--function guardOrKey(capability) {
-- const tokenGuard = guard(capability);
-- return function liveServiceGuardOrKey(req, res, next) {
-- if (String(req.headers.authorization || '').startsWith('Bearer ')) return tokenGuard(req, res, next);
-- if (require('./internal-key').internalKeyOk(req)) return next();
-- return http.sendProblem(res, 401, 'token.missing', { detail: 'a service token is required', ctx: http.requestContext(req.headers) });
-- };
--}
--
--module.exports = { guard, guardOrKey, AUDIENCE };
++module.exports = { guard, viaProxy, AUDIENCE };
ddiff --git a/test/chat-tables.test.js b/test/chat-tables.test.js
iindex 94a0e94d..f6d6f5d9 100644
---- a/test/chat-tables.test.js
++++ b/test/chat-tables.test.js
@@@ -28,7 +28,6 @@ process.env.OV_NETWORK_PUBLIC_KEY = path.join(tmp, 'network.pem');
process.env.OV_NETWORK_URL = 'https://openvibe.network';
process.env.OV_OAUTH_CLIENT_ID = 'live';
process.env.OV_OAUTH_CLIENT_SECRET = 'live-secret';
--process.env.INTERNAL_API_KEY = 'internal-test-key';
process.env.CHAT_AUTHORITY = 'chat';
const quiet = console.log;
console.log = () => {};
@@@ -183,8 +182,9 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let json = null; try { json = JSON.parse(text); } catch { /* */ }
return { status: res.status, body: json, text };
};
-- const KEY = { 'X-Internal-Key': 'internal-test-key' };
-- const internal = (method, p, body) => call(method, `/internal${p}`, { body, headers: KEY });
++ // OpenVibe.Chat's token for the staged-table handoff (live.chat_mirror.write); X-Internal-Key is gone (plan T2).
++ const CHAT = () => ({ Authorization: `Bearer ${serviceAuth.signServiceToken({ iss: ISS, sub: 'svc:chat', actor_type: 'service', aud: ['openvibe.live'], cap: ['live.chat_mirror.write'], iat: now(), exp: now() + 300, jti: `tok_test_${++jti}` }, keys.privateKey)}` });
++ const internal = (method, p, body) => call(method, `/internal${p}`, { body, headers: CHAT() });
const MIRROR = serviceToken(['live.chat_mirror.write'], 'openvibe.live');
const mirror = (changes) => call('POST', '/internal/chat-effects/mirror', { body: { changes }, headers: { Authorization: `Bearer ${MIRROR}` } });
@@@ -285,9 +285,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
assert.strictEqual(stats('channel_moderation_settings').compared, 2);
assert.strictEqual((await internal('POST', '/chat-tables/channel_moderation_settings', { reset_counters: true })).body.table.dual_read_stats.compared, 0);
-- // 6. The status route: loopback + internal key; both sides' view of every table.
++ // 6. The status route: loopback + a service token (live.chat_mirror.write); both sides' view of every table.
assert.strictEqual((await call('GET', '/internal/chat-tables')).status, 401, 'no credentials');
-- assert.strictEqual((await call('GET', '/internal/chat-tables', { headers: { ...KEY, 'X-Forwarded-For': '203.0.113.1' } })).status, 401, 'the key through the public edge');
++ assert.strictEqual((await call('GET', '/internal/chat-tables', { headers: { ...CHAT(), 'X-Forwarded-For': '203.0.113.1' } })).status, 403, 'a token through the public edge');
++ assert.strictEqual((await call('GET', '/internal/chat-tables', { headers: { 'X-Internal-Key': 'k'.repeat(40) } })).status, 401, 'the retired key opens nothing');
let s = (await internal('GET', '/chat-tables')).body;
assert.strictEqual(s.chat_authority, 'chat');
assert.deepStrictEqual([s.tables.emotes.authority, s.tables.emotes.chat_authority, s.tables.emotes.dual_read, s.tables.emotes.outbox_pending], ['live', 'live', true, 0]);
ddiff --git a/test/cosmetics-internal.test.js b/test/cosmetics-internal.test.js
iindex 1eb6d8d4..2bb270ff 100644
---- a/test/cosmetics-internal.test.js
++++ b/test/cosmetics-internal.test.js
@@@ -1,11 +1,11 @@
'use strict';
--// Live's internal (server-to-server) routes, during the X-Internal-Key retirement (plan T2):
++// Live's internal (server-to-server) routes after the X-Internal-Key retirement (plan T2):
// - each route checks the one capability it performs on a Network service token (server/net/service-guard.js);
--// - a request that presents a Bearer is judged on the token alone, never downgraded to the key;
--// - without a Bearer, the key still passes from loopback while callers move (guardOrKey), compared in constant time;
--// - nothing that came through nginx or Cloudflare gets in, with a token or the key;
--// - POST /api/cosmetics/internal-unlock (the openvibe-quest bridge) is gone: the quest game runs nowhere.
++// - the retired key opens nothing, alone or next to a bad token;
++// - nothing that came through nginx or Cloudflare gets in;
++// - POST /api/cosmetics/internal-unlock (the openvibe-quest bridge) is gone: the quest game runs nowhere;
++// - no server file reads INTERNAL_API_KEY any more.
const assert = require('assert');
const crypto = require('crypto');
@@@ -21,7 +21,7 @@ fs.writeFileSync(path.join(tmp, 'network.pem'), keys.publicKey);
process.env.DB_PATH = path.join(tmp, 'live.db');
process.env.OV_NETWORK_PUBLIC_KEY = path.join(tmp, 'network.pem');
process.env.OV_NETWORK_URL = 'https://openvibe.network';
--const KEY = 'k'.repeat(40);
++const KEY = 'k'.repeat(40); // what an old caller would send: it must open nothing
process.env.INTERNAL_API_KEY = KEY;
console.log = () => {};
console.warn = () => {};
@@@ -58,15 +58,13 @@ const token = (cap, { aud = 'openvibe.live', sub = 'svc:network', env } = {}) =>
assert.strictEqual((await avatar(bearer(token(['live.url_registry.refresh'])))).status, 403, 'another capability is not enough');
assert.strictEqual((await avatar(bearer(token(['live.avatar.write'], { aud: 'openvibe.network' })))).status, 401, 'a token for another audience');
assert.strictEqual((await avatar(bearer(token(['live.avatar.write'], { env: 'sandbox' })))).status, 401, 'a sandbox token');
-- assert.strictEqual((await avatar({ ...bearer('not.a.token'), 'x-internal-key': KEY })).status, 401, 'a Bearer is judged on the token alone, never downgraded to the key');
++ assert.strictEqual((await avatar({ ...bearer('not.a.token'), 'x-internal-key': KEY })).status, 401, 'a bad token is not rescued by the key');
assert.strictEqual((await avatar({ ...bearer(token(['live.avatar.write'])), 'x-forwarded-for': '203.0.113.9' })).status, 403, 'a token through the public edge is refused');
assert.strictEqual((await avatar({})).status, 401);
-- // The key while callers move: loopback only, constant time.
-- assert.strictEqual((await avatar({ 'x-internal-key': KEY })).status, 404, 'the key from loopback still reaches the handler');
-- assert.strictEqual((await avatar({ 'x-internal-key': 'j'.repeat(40) })).status, 401, 'a wrong key of the right length');
-- assert.strictEqual((await avatar({ 'x-internal-key': KEY + 'x' })).status, 401, 'a longer key that starts right');
++ // The retired key opens nothing.
++ assert.strictEqual((await avatar({ 'x-internal-key': KEY })).status, 401, 'the key alone is refused');
for (const h of ['x-forwarded-for', 'x-real-ip', 'cf-connecting-ip']) {
-- assert.strictEqual((await avatar({ 'x-internal-key': KEY, [h]: '203.0.113.9' })).status, 401, `the right key through the public edge (${h}) is refused`);
++ assert.strictEqual((await avatar({ ...bearer(token(['live.avatar.write'])), [h]: '203.0.113.9' })).status, 403, `a token through the public edge (${h}) is refused`);
}
// ── the other internal routes: each has its own capability ───────────────────
@@@ -77,25 +75,20 @@ const token = (cap, { aud = 'openvibe.live', sub = 'svc:network', env } = {}) =>
assert.notStrictEqual((await tables(bearer(token(['live.chat_mirror.write'], { sub: 'svc:chat' })))).status, 403, 'Chat opens the staged-table handoff with live.chat_mirror.write');
assert.strictEqual((await tables({})).status, 401);
-- const ik = require('../server/net/internal-key');
-- assert.strictEqual(ik.internalKeyMatches(KEY), true);
-- assert.strictEqual(ik.internalKeyMatches(''), false);
-- assert.strictEqual(ik.internalKeyMatches(KEY.slice(0, 39)), false);
--
-- // No internal-key compare with === / !== anywhere in the server.
++ // No server file reads or sends the retired key.
const offenders = [];
const walk = (dir) => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (e.name.endsWith('.js')) {
-- const src = fs.readFileSync(p, 'utf8');
-- if (/[!=]==\s*(config\.internalApiKey|INTERNAL_API_KEY)\b|\b(config\.internalApiKey|INTERNAL_API_KEY)\s*[!=]==/.test(src)) offenders.push(path.relative(path.join(__dirname, '..'), p));
++ const src = fs.readFileSync(p, 'utf8').replace(/^\s*(\/\/|\*).*$/gm, ''); // code, not comments
++ if (/INTERNAL_API_KEY|OV_INTERNAL_KEY|internalApiKey|x-internal-key/i.test(src)) offenders.push(path.relative(path.join(__dirname, '..'), p));
}
}
};
walk(path.join(__dirname, '..', 'server'));
-- assert.deepStrictEqual(offenders, [], 'internal keys are compared with internalKeyMatches (constant time)');
++ assert.deepStrictEqual(offenders, [], 'no server file reads or sends X-Internal-Key');
process.stdout.write('live internal routes: all checks passed\n');
} finally {
server.close();