openvibers/openvibe.codes Commits
Pictures reach Media with Codes' Network service token, not an app key (#191)
Changes
@@@ -113,10 +113,10 @@ function load(env = process.env) {},// OpenVibe.Media: the private objects a task's pictures live in. A proof screenshot can show anything on the// person's screen, so it is never public: the bytes go to Media as a private object of the person, and the-- // session shows a short-lived signed URL that only that person can use. Without a key there are no pictures.++ // session shows a short-lived signed URL that only that person can use. Media takes no app keys (C-71): Codes++ // uses its Network service token (server/media.js), so without a client secret there are no pictures.media: {url: trim(env.CODES_MEDIA_URL || 'https://openvibe.media'),-- key: String(env.CODES_MEDIA_KEY || '').trim(),app: trim(env.CODES_MEDIA_APP || 'codes'),ttl: int(env.CODES_MEDIA_URL_TTL, 600),},@@@ -97,7 +97,7 @@ function lineOf(raw) {function createSessionLines({ store, config = {}, log = console, media = null }) {const db = store.db;const retentionDays = Math.max(1, Number(config.sessions && config.sessions.retentionDays) || 90);-- const pictures = media && media.configured ? media : null;++ const pictures = media && typeof media.configured === 'function' && media.configured() ? media : null;/** The bytes of a data URL, or null when it is not one. */function bytesOf(url) {@@@ -4,7 +4,9 @@** A proof screenshot can show anything on the person's screen, so it never becomes a public file. The bytes go to* Media as a private object owned by the person, and the session shows a short-lived signed URL that only that-- * person (through Codes) can use. Without a key there are no attachments, and the session simply shows the text.++ * person (through Codes) can use. Media takes no app keys (C-71): Codes reaches its own tenant with its Network service++ * token (audience openvibe.media; media.object.upload and media.object.read, granted on the codes tenant only), the++ * person named in X-OV-Subject. Without a client secret there are no attachments, and the session simply shows the text.** createMedia({ config, fetchImpl })* configured() is Media set up?@@@ -13,22 +15,28 @@* uploadAttachments(list, subject) → the list with the bytes swapped for object ids* signAttachments(list, subject) → the list with the object ids swapped for URLs*/++const { createServiceTokenClient } = require('openvibe-sdk/auth');++const DEFAULT_TTL = 900; // a signed URL lives fifteen minutesconst MAX_BYTES = 12 * 1024 * 1024;++const SCOPE = 'media.object.upload media.object.read';--function createMedia({ config, fetchImpl = globalThis.fetch, log = null } = {}) {++function createMedia({ config, fetchImpl = globalThis.fetch, log = null, tokens = null } = {}) {const media = (config && config.media) || {};++ const oauth = (config && config.oauth) || {};const url = String(media.url || '').replace(/\/+$/, '');-- const key = String(media.key || '').trim();const app = String(media.app || 'codes').trim() || 'codes';const ttl = Math.min(3600, Math.max(60, Number(media.ttl) || DEFAULT_TTL));const base = () => `${url}/api/v2/${encodeURIComponent(app)}/objects`;-- const configured = () => !!(url && key);++ const auth = tokens || (oauth.clientSecret && config.networkInternalUrl++ ? createServiceTokenClient({ tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: oauth.clientId, clientSecret: oauth.clientSecret, audience: 'openvibe.media', scope: SCOPE, fetch: fetchImpl })++ : null);++ const configured = () => !!(url && auth);const warn = (msg) => { if (log && typeof log.warn === 'function') log.warn(msg); };async function call(method, apiUrl, subject, body, raw) {-- const headers = { authorization: `Bearer ${key}`, accept: 'application/json' };++ const headers = { ...(await auth.authHeaders()), accept: 'application/json' };if (subject) headers['x-ov-subject'] = String(subject);if (body != null) headers['content-type'] = 'application/json';const ctrl = new AbortController();@@@ -39,6 +47,7 @@ function createMedia({ config, fetchImpl = globalThis.fetch, log = null } = {})} finally {clearTimeout(timer);}++ if (res.status === 401 && auth.invalidate) auth.invalidate();const text = await res.text();let json = null;try { json = text ? JSON.parse(text) : null; } catch { json = null; }@@@ -2,7 +2,8 @@/*** The pictures of a task (Codes C10): the images a harness read and the screenshots the proof shot travel in the job* result, and Codes puts them in Media as private objects of the person — never a public URL. The session shows a-- * short-lived signed URL, and only the task's owner gets one: another person gets a 404.++ * short-lived signed URL, and only the task's owner gets one: another person gets a 404. Media takes no app keys++ * (C-71), so every call carries Codes' Network service token for openvibe.media, upload and read only.*/const assert = require('assert');const http = require('http');@@@ -48,7 +49,7 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');await new Promise((ok) => mediaServer.listen(0, '127.0.0.1', ok));const mediaUrl = `http://127.0.0.1:${mediaServer.address().port}`;-- const t = await boot({ env: { CODES_MEDIA_URL: mediaUrl, CODES_MEDIA_KEY: 'codes-test-key' } });++ const t = await boot({ env: { CODES_MEDIA_URL: mediaUrl } });const db = t.ctx.store.db;const ada = t.network.addUser('ada');const bo = t.network.addUser('bo');@@@ -58,7 +59,7 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');VALUES ($1, $2, $3, 'job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J55', 'https://github.com/a/b', 'look at the picture', 'running', 1, 2)`,[taskId, ada.subject, project]);-- const media = createMedia({ config: { media: { url: mediaUrl, key: 'codes-test-key', app: 'codes', ttl: 600 } }, log: console });++ const media = createMedia({ config: t.ctx.config, log: console });const lines = createSessionLines({ store: t.ctx.store, config: t.ctx.config, log: console, media });const proof = createProof({ store: t.ctx.store, config: t.ctx.config, log: console });@@@ -75,6 +76,10 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');assert.strictEqual(body.visibility, 'private', 'a screenshot is never public');assert.strictEqual(body.mime_type, 'image/png', 'Media is told what it is');assert.ok(init.headers['x-ov-subject'] === ada.subject, 'the object belongs to the person the task belongs to');++ assert.ok(/^Bearer ey/.test(String(init.headers.authorization)), 'Codes signs in to Media with a Network token, never an app key');++ const asked = t.network.tokenRequests.filter((r) => r.audience === 'openvibe.media');++ assert.ok(asked.length >= 1, 'the token is for Media');++ assert.deepStrictEqual(asked.map((r) => [r.grant_type, r.client_id, r.scope, r.on_behalf_of]).pop(), ['client_credentials', 'codes', 'media.object.upload media.object.read', null], 'Codes\' own service token, upload and read only, acting for nobody');assert.ok(calls.some((c) => c.method === 'PUT' && c.url.startsWith('/upload/')), 'the bytes were sent');assert.ok(calls.some((c) => c.method === 'POST' && c.url.endsWith('/complete')), 'and the object was completed');@@@ -96,8 +101,21 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');await check('another person cannot read the task at all', async () => {const res = await t.get(`/run/tasks/${taskId}/session.json`, { as: bo });assert.strictEqual(res.status, 404, 'a task of yours is 404 for another person');-- const row = await db.maybe('SELECT id, subject FROM run_tasks WHERE id = $1', [taskId]);-- process.stderr.write(`[test] the row: ${JSON.stringify(row)}; ada: ${ada.subject}\n`);++ });++++ await check('without a client secret Codes keeps no pictures, and never falls back to a key', async () => {++ const off = createMedia({ config: { media: { url: mediaUrl, app: 'codes' }, oauth: { clientId: 'codes', clientSecret: '' }, networkInternalUrl: t.network.url } });++ assert.strictEqual(off.configured(), false, 'no secret, no Media');++ const offLines = createSessionLines({ store: t.ctx.store, config: t.ctx.config, log: console, media: off });++ calls.length = 0;++ await offLines.append(taskId, [{++ seq: 2, at: new Date().toISOString(), kind: 'tool_result',++ body: { t: 'tool_result', id: 'tool-2', ok: true, output: 'still here', attachments: [{ url: `data:image/png;base64,${PNG}`, name: 'b.png', mime: 'image/png', kind: 'image', role: 'read' }] },++ }], null, ada.subject);++ assert.strictEqual(calls.length, 0, 'Media is never called');++ const row = await db.maybe("SELECT body FROM codes_session_lines WHERE task_id = $1 AND body->>'id' = 'tool-2'", [taskId]);++ assert.ok(row && row.body.output === 'still here', 'the line keeps its words');++ assert.ok(!row.body.attachments && !JSON.stringify(row.body).includes('base64'), 'and drops the picture, bytes and all');});await check('a proof item with a screenshot puts the picture on its line and keeps no bytes in the record', async () => {@@@ -50,7 +50,7 @@ const INTERNAL = [`http://127.0.0.1:3000${PROBE}`, `http://2130706433${PROBE}`,'server/http/tasks-api.js': 'OpenVibe.Bench (CODES_BENCH_URL, configured), Network projects API (configured) with the verified user token; benchmark ids and task ids are URL-encoded, no URL from a person','server/http/pages.js': 'Network projects API (configured) for Improve OpenVibe project creation; Forgejo repo read (CODES_GIT_INTERNAL_URL) for repo details; no URL from a person','server/domain/connections.js': 'OpenVibe.AI (CODES_AI_URL, configured) for a person\'s own keys: the credential routes and the two model endpoints, with the provider and the subject as path segments; no URL from a person',-- 'server/media.js': 'OpenVibe.Media\'s objects API (CODES_MEDIA_URL, configured) with the service key, and the presigned upload URL Media itself answers for that object; never a URL from a person or a model',++ 'server/media.js': 'OpenVibe.Media\'s objects API (CODES_MEDIA_URL, configured) with Codes\' Network service token, and the presigned upload URL Media itself answers for that object; never a URL from a person or a model','server/domain/run-tokens.js': 'OpenVibe.Network\'s token endpoint (OV_NETWORK_INTERNAL_URL, configured) for a run token; no URL from a person','server/domain/machines.js': 'OpenVibe.Network (OV_NETWORK_INTERNAL_URL, configured) /api/v1/me/nodes with the person\'s own session token: their machines and a revoke; the path id is checked against a nod_ shape, never a URL from a person','server/domain/conductor.js': 'OpenVibe.Network\'s projects API (OV_NETWORK_INTERNAL_URL, configured) with the person\'s own token, for the project a conductor runs in; and the Run job stream URL run-client.js builds on its own base. No URL from a person',
@@ -113,10 +113,10 @@ function load(env = process.env) {
@@ -113,10 +113,10 @@ function load(env = process.env) {
},
},
// OpenVibe.Media: the private objects a task's pictures live in. A proof screenshot can show anything on the
// OpenVibe.Media: the private objects a task's pictures live in. A proof screenshot can show anything on the
// person's screen, so it is never public: the bytes go to Media as a private object of the person, and the
// person's screen, so it is never public: the bytes go to Media as a private object of the person, and the
- // session shows a short-lived signed URL that only that person can use. Without a key there are no pictures.
+ // session shows a short-lived signed URL that only that person can use. Media takes no app keys (C-71): Codes
+ // uses its Network service token (server/media.js), so without a client secret there are no pictures.
media: {
media: {
url: trim(env.CODES_MEDIA_URL || 'https://openvibe.media'),
url: trim(env.CODES_MEDIA_URL || 'https://openvibe.media'),
- key: String(env.CODES_MEDIA_KEY || '').trim(),
app: trim(env.CODES_MEDIA_APP || 'codes'),
app: trim(env.CODES_MEDIA_APP || 'codes'),
ttl: int(env.CODES_MEDIA_URL_TTL, 600),
ttl: int(env.CODES_MEDIA_URL_TTL, 600),
},
},
@@ -97,7 +97,7 @@ function lineOf(raw) {
@@ -97,7 +97,7 @@ function lineOf(raw) {
function createSessionLines({ store, config = {}, log = console, media = null }) {
function createSessionLines({ store, config = {}, log = console, media = null }) {
const db = store.db;
const db = store.db;
const retentionDays = Math.max(1, Number(config.sessions && config.sessions.retentionDays) || 90);
const retentionDays = Math.max(1, Number(config.sessions && config.sessions.retentionDays) || 90);
- const pictures = media && media.configured ? media : null;
+ const pictures = media && typeof media.configured === 'function' && media.configured() ? media : null;
/** The bytes of a data URL, or null when it is not one. */
/** The bytes of a data URL, or null when it is not one. */
function bytesOf(url) {
function bytesOf(url) {
@@ -4,7 +4,9 @@
@@ -4,7 +4,9 @@
*
*
* A proof screenshot can show anything on the person's screen, so it never becomes a public file. The bytes go to
* A proof screenshot can show anything on the person's screen, so it never becomes a public file. The bytes go to
* Media as a private object owned by the person, and the session shows a short-lived signed URL that only that
* Media as a private object owned by the person, and the session shows a short-lived signed URL that only that
- * person (through Codes) can use. Without a key there are no attachments, and the session simply shows the text.
+ * person (through Codes) can use. Media takes no app keys (C-71): Codes reaches its own tenant with its Network service
+ * token (audience openvibe.media; media.object.upload and media.object.read, granted on the codes tenant only), the
+ * person named in X-OV-Subject. Without a client secret there are no attachments, and the session simply shows the text.
*
*
* createMedia({ config, fetchImpl })
* createMedia({ config, fetchImpl })
* configured() is Media set up?
* configured() is Media set up?
@@ -13,22 +15,28 @@
@@ -13,22 +15,28 @@
* uploadAttachments(list, subject) → the list with the bytes swapped for object ids
* uploadAttachments(list, subject) → the list with the bytes swapped for object ids
* signAttachments(list, subject) → the list with the object ids swapped for URLs
* signAttachments(list, subject) → the list with the object ids swapped for URLs
*/
*/
+const { createServiceTokenClient } = require('openvibe-sdk/auth');
+
const DEFAULT_TTL = 900; // a signed URL lives fifteen minutes
const DEFAULT_TTL = 900; // a signed URL lives fifteen minutes
const MAX_BYTES = 12 * 1024 * 1024;
const MAX_BYTES = 12 * 1024 * 1024;
+const SCOPE = 'media.object.upload media.object.read';
-function createMedia({ config, fetchImpl = globalThis.fetch, log = null } = {}) {
+function createMedia({ config, fetchImpl = globalThis.fetch, log = null, tokens = null } = {}) {
const media = (config && config.media) || {};
const media = (config && config.media) || {};
+ const oauth = (config && config.oauth) || {};
const url = String(media.url || '').replace(/\/+$/, '');
const url = String(media.url || '').replace(/\/+$/, '');
- const key = String(media.key || '').trim();
const app = String(media.app || 'codes').trim() || 'codes';
const app = String(media.app || 'codes').trim() || 'codes';
const ttl = Math.min(3600, Math.max(60, Number(media.ttl) || DEFAULT_TTL));
const ttl = Math.min(3600, Math.max(60, Number(media.ttl) || DEFAULT_TTL));
const base = () => `${url}/api/v2/${encodeURIComponent(app)}/objects`;
const base = () => `${url}/api/v2/${encodeURIComponent(app)}/objects`;
- const configured = () => !!(url && key);
+ const auth = tokens || (oauth.clientSecret && config.networkInternalUrl
+ ? createServiceTokenClient({ tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: oauth.clientId, clientSecret: oauth.clientSecret, audience: 'openvibe.media', scope: SCOPE, fetch: fetchImpl })
+ : null);
+ const configured = () => !!(url && auth);
const warn = (msg) => { if (log && typeof log.warn === 'function') log.warn(msg); };
const warn = (msg) => { if (log && typeof log.warn === 'function') log.warn(msg); };
async function call(method, apiUrl, subject, body, raw) {
async function call(method, apiUrl, subject, body, raw) {
- const headers = { authorization: `Bearer ${key}`, accept: 'application/json' };
+ const headers = { ...(await auth.authHeaders()), accept: 'application/json' };
if (subject) headers['x-ov-subject'] = String(subject);
if (subject) headers['x-ov-subject'] = String(subject);
if (body != null) headers['content-type'] = 'application/json';
if (body != null) headers['content-type'] = 'application/json';
const ctrl = new AbortController();
const ctrl = new AbortController();
@@ -39,6 +47,7 @@ function createMedia({ config, fetchImpl = globalThis.fetch, log = null } = {})
@@ -39,6 +47,7 @@ function createMedia({ config, fetchImpl = globalThis.fetch, log = null } = {})
} finally {
} finally {
clearTimeout(timer);
clearTimeout(timer);
}
}
+ if (res.status === 401 && auth.invalidate) auth.invalidate();
const text = await res.text();
const text = await res.text();
let json = null;
let json = null;
try { json = text ? JSON.parse(text) : null; } catch { json = null; }
try { json = text ? JSON.parse(text) : null; } catch { json = null; }
@@ -2,7 +2,8 @@
@@ -2,7 +2,8 @@
/**
/**
* The pictures of a task (Codes C10): the images a harness read and the screenshots the proof shot travel in the job
* The pictures of a task (Codes C10): the images a harness read and the screenshots the proof shot travel in the job
* result, and Codes puts them in Media as private objects of the person — never a public URL. The session shows a
* result, and Codes puts them in Media as private objects of the person — never a public URL. The session shows a
- * short-lived signed URL, and only the task's owner gets one: another person gets a 404.
+ * short-lived signed URL, and only the task's owner gets one: another person gets a 404. Media takes no app keys
+ * (C-71), so every call carries Codes' Network service token for openvibe.media, upload and read only.
*/
*/
const assert = require('assert');
const assert = require('assert');
const http = require('http');
const http = require('http');
@@ -48,7 +49,7 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
@@ -48,7 +49,7 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
await new Promise((ok) => mediaServer.listen(0, '127.0.0.1', ok));
await new Promise((ok) => mediaServer.listen(0, '127.0.0.1', ok));
const mediaUrl = `http://127.0.0.1:${mediaServer.address().port}`;
const mediaUrl = `http://127.0.0.1:${mediaServer.address().port}`;
- const t = await boot({ env: { CODES_MEDIA_URL: mediaUrl, CODES_MEDIA_KEY: 'codes-test-key' } });
+ const t = await boot({ env: { CODES_MEDIA_URL: mediaUrl } });
const db = t.ctx.store.db;
const db = t.ctx.store.db;
const ada = t.network.addUser('ada');
const ada = t.network.addUser('ada');
const bo = t.network.addUser('bo');
const bo = t.network.addUser('bo');
@@ -58,7 +59,7 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
@@ -58,7 +59,7 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
VALUES ($1, $2, $3, 'job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J55', 'https://github.com/a/b', 'look at the picture', 'running', 1, 2)`,
VALUES ($1, $2, $3, 'job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J55', 'https://github.com/a/b', 'look at the picture', 'running', 1, 2)`,
[taskId, ada.subject, project]);
[taskId, ada.subject, project]);
- const media = createMedia({ config: { media: { url: mediaUrl, key: 'codes-test-key', app: 'codes', ttl: 600 } }, log: console });
+ const media = createMedia({ config: t.ctx.config, log: console });
const lines = createSessionLines({ store: t.ctx.store, config: t.ctx.config, log: console, media });
const lines = createSessionLines({ store: t.ctx.store, config: t.ctx.config, log: console, media });
const proof = createProof({ store: t.ctx.store, config: t.ctx.config, log: console });
const proof = createProof({ store: t.ctx.store, config: t.ctx.config, log: console });
@@ -75,6 +76,10 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
@@ -75,6 +76,10 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
assert.strictEqual(body.visibility, 'private', 'a screenshot is never public');
assert.strictEqual(body.visibility, 'private', 'a screenshot is never public');
assert.strictEqual(body.mime_type, 'image/png', 'Media is told what it is');
assert.strictEqual(body.mime_type, 'image/png', 'Media is told what it is');
assert.ok(init.headers['x-ov-subject'] === ada.subject, 'the object belongs to the person the task belongs to');
assert.ok(init.headers['x-ov-subject'] === ada.subject, 'the object belongs to the person the task belongs to');
+ assert.ok(/^Bearer ey/.test(String(init.headers.authorization)), 'Codes signs in to Media with a Network token, never an app key');
+ const asked = t.network.tokenRequests.filter((r) => r.audience === 'openvibe.media');
+ assert.ok(asked.length >= 1, 'the token is for Media');
+ assert.deepStrictEqual(asked.map((r) => [r.grant_type, r.client_id, r.scope, r.on_behalf_of]).pop(), ['client_credentials', 'codes', 'media.object.upload media.object.read', null], 'Codes\' own service token, upload and read only, acting for nobody');
assert.ok(calls.some((c) => c.method === 'PUT' && c.url.startsWith('/upload/')), 'the bytes were sent');
assert.ok(calls.some((c) => c.method === 'PUT' && c.url.startsWith('/upload/')), 'the bytes were sent');
assert.ok(calls.some((c) => c.method === 'POST' && c.url.endsWith('/complete')), 'and the object was completed');
assert.ok(calls.some((c) => c.method === 'POST' && c.url.endsWith('/complete')), 'and the object was completed');
@@ -96,8 +101,21 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
@@ -96,8 +101,21 @@ const b64 = (bytes) => Buffer.alloc(bytes, 7).toString('base64');
await check('another person cannot read the task at all', async () => {
await check('another person cannot read the task at all', async () => {
const res = await t.get(`/run/tasks/${taskId}/session.json`, { as: bo });
const res = await t.get(`/run/tasks/${taskId}/session.json`, { as: bo });
assert.strictEqual(res.status, 404, 'a task of yours is 404 for another person');
assert.strictEqual(res.status, 404, 'a task of yours is 404 for another person');
- const row = await db.maybe('SELECT id, subject FROM run_tasks WHERE id = $1', [taskId]);
- process.stderr.write(`[test] the row: ${JSON.stringify(row)}; ada: ${ada.subject}\n`);
+ });
+
+ await check('without a client secret Codes keeps no pictures, and never falls back to a key', async () => {
+ const off = createMedia({ config: { media: { url: mediaUrl, app: 'codes' }, oauth: { clientId: 'codes', clientSecret: '' }, networkInternalUrl: t.network.url } });
+ assert.strictEqual(off.configured(), false, 'no secret, no Media');
+ const offLines = createSessionLines({ store: t.ctx.store, config: t.ctx.config, log: console, media: off });
+ calls.length = 0;
+ await offLines.append(taskId, [{
+ seq: 2, at: new Date().toISOString(), kind: 'tool_result',
+ body: { t: 'tool_result', id: 'tool-2', ok: true, output: 'still here', attachments: [{ url: `data:image/png;base64,${PNG}`, name: 'b.png', mime: 'image/png', kind: 'image', role: 'read' }] },
+ }], null, ada.subject);
+ assert.strictEqual(calls.length, 0, 'Media is never called');
+ const row = await db.maybe("SELECT body FROM codes_session_lines WHERE task_id = $1 AND body->>'id' = 'tool-2'", [taskId]);
+ assert.ok(row && row.body.output === 'still here', 'the line keeps its words');
+ assert.ok(!row.body.attachments && !JSON.stringify(row.body).includes('base64'), 'and drops the picture, bytes and all');
});
});
await check('a proof item with a screenshot puts the picture on its line and keeps no bytes in the record', async () => {
await check('a proof item with a screenshot puts the picture on its line and keeps no bytes in the record', async () => {
@@ -50,7 +50,7 @@ const INTERNAL = [`http://127.0.0.1:3000${PROBE}`, `http://2130706433${PROBE}`,
@@ -50,7 +50,7 @@ const INTERNAL = [`http://127.0.0.1:3000${PROBE}`, `http://2130706433${PROBE}`,
'server/http/tasks-api.js': 'OpenVibe.Bench (CODES_BENCH_URL, configured), Network projects API (configured) with the verified user token; benchmark ids and task ids are URL-encoded, no URL from a person',
'server/http/tasks-api.js': 'OpenVibe.Bench (CODES_BENCH_URL, configured), Network projects API (configured) with the verified user token; benchmark ids and task ids are URL-encoded, no URL from a person',
'server/http/pages.js': 'Network projects API (configured) for Improve OpenVibe project creation; Forgejo repo read (CODES_GIT_INTERNAL_URL) for repo details; no URL from a person',
'server/http/pages.js': 'Network projects API (configured) for Improve OpenVibe project creation; Forgejo repo read (CODES_GIT_INTERNAL_URL) for repo details; no URL from a person',
'server/domain/connections.js': 'OpenVibe.AI (CODES_AI_URL, configured) for a person\'s own keys: the credential routes and the two model endpoints, with the provider and the subject as path segments; no URL from a person',
'server/domain/connections.js': 'OpenVibe.AI (CODES_AI_URL, configured) for a person\'s own keys: the credential routes and the two model endpoints, with the provider and the subject as path segments; no URL from a person',
- 'server/media.js': 'OpenVibe.Media\'s objects API (CODES_MEDIA_URL, configured) with the service key, and the presigned upload URL Media itself answers for that object; never a URL from a person or a model',
+ 'server/media.js': 'OpenVibe.Media\'s objects API (CODES_MEDIA_URL, configured) with Codes\' Network service token, and the presigned upload URL Media itself answers for that object; never a URL from a person or a model',
'server/domain/run-tokens.js': 'OpenVibe.Network\'s token endpoint (OV_NETWORK_INTERNAL_URL, configured) for a run token; no URL from a person',
'server/domain/run-tokens.js': 'OpenVibe.Network\'s token endpoint (OV_NETWORK_INTERNAL_URL, configured) for a run token; no URL from a person',
'server/domain/machines.js': 'OpenVibe.Network (OV_NETWORK_INTERNAL_URL, configured) /api/v1/me/nodes with the person\'s own session token: their machines and a revoke; the path id is checked against a nod_ shape, never a URL from a person',
'server/domain/machines.js': 'OpenVibe.Network (OV_NETWORK_INTERNAL_URL, configured) /api/v1/me/nodes with the person\'s own session token: their machines and a revoke; the path id is checked against a nod_ shape, never a URL from a person',
'server/domain/conductor.js': 'OpenVibe.Network\'s projects API (OV_NETWORK_INTERNAL_URL, configured) with the person\'s own token, for the project a conductor runs in; and the Run job stream URL run-client.js builds on its own base. No URL from a person',
'server/domain/conductor.js': 'OpenVibe.Network\'s projects API (OV_NETWORK_INTERNAL_URL, configured) with the person\'s own token, for the project a conductor runs in; and the Run job stream URL run-client.js builds on its own base. No URL from a person',