openvibers/openvibe.exchange Commits
Answer the companions and privacy/keys twins, drop the owner-only console link (#132)
Changes
@@@ -3,6 +3,7 @@// own render through the same views and layout the HTML routes use, always signed out: a chat, a try box and the// 18+ confirmation never appear in a twin.const views = require('./views/exchange');++const companionViews = require('./views/companions');const arenaViews = require('./views/arena');const { CATEGORIES } = require('./arena/category');const { ARENA_KIND } = require('./bench');@@@ -13,7 +14,7 @@ const { homeJsonLd } = require('./http/discovery');const HOME_DESCRIPTION = 'OpenVibe.Exchange: every AI model, one account. Live prices, speed and uptime of every offer, from the families on sale today and more as their providers join; chat with one model or three side by side; one OpenAI- and Anthropic-compatible endpoint.';function createAgentPages(ctx) {-- const { config, ai, bench, history } = ctx;++ const { config, ai, bench, history, characters } = ctx;const page = (path, o) => ({ html: renderPage({ viewer: { kind: 'anonymous' }, config, path, agentLayer: true, ...o }), title: o.title });async function home(path) {@@@ -83,6 +84,30 @@ function createAgentPages(ctx) {]);return page(p, { index: true, title: 'Status', description: 'Uptime per offer, the model changelog, and incidents.', body: raw(views.statusPage({ catalog, usage, changes })) });}++ if (p === '/companions') {++ // The public gallery, always the signed-out view: no "Your characters", no adult cards.++ const [gallery, tagList] = await Promise.all([++ characters.gallery({ sort: 'new', q: '', tags: [], limit: 48, allowAdult: false }),++ characters.allTags(24),++ ]);++ return page(p, {++ index: true, title: 'Characters',++ description: 'Character cards on OpenVibe.Exchange: bring a card in as Character Card V2 or V3, talk to it, and take it with you again.',++ body: raw(companionViews.galleryPage({++ items: gallery.items, total: gallery.total, q: '', tags: [], sort: 'new', tagList,++ viewer: { kind: 'anonymous' }, adult: false, mine: [], defaults: '',++ })),++ });++ }++ if (p === '/privacy/keys') {++ // Signed out, /privacy/keys only sends a person to sign in; the twin says that and nothing of an++ // account, a credential or a key.++ return page(p, {++ index: false, title: 'Your keys and accounts',++ description: 'Sign in to manage your own provider keys and supported browser session cookies.',++ body: raw('<div class="ex-wrap ex-narrow"><header class="ex-companions-head"><h1>Your keys and accounts</h1><p class="ex-lede">Sign in to manage your own provider keys and supported browser session cookies.</p><p class="ex-head-actions"><a class="ovk-btn ovk-btn--primary" href="/auth/login?next=%2Fprivacy%2Fkeys">Sign in with OpenVibe</a></p></header></div>'),++ });++ }return null;}@@@ -127,7 +127,6 @@ function createDiscoveryRoutes(ctx) {{ title: 'Full text for language models', url: abs('/llms-full.txt') },{ title: 'Release metadata (JSON)', url: abs('/release.json') },{ title: 'AI router API', url: 'https://ai.openvibe.services', note: 'the API and run receipts' },-- { title: 'AI operator console', url: abs('/console'), note: 'owner-only controls for providers, models, routes, quotas, usage and runs' },{ title: 'Your keys and accounts', url: abs('/privacy/keys'), note: 'manage your own provider keys and supported browser session cookies' },{ title: 'OpenVibe Link', url: 'https://openvibe.link', note: 'offer a machine to the network' },],@@@ -49,6 +49,27 @@ const { createFakeAi } = require('./helpers/ai');assert.strictEqual((await t.get('/no-such-page.md')).status, 404);});++ await check('the companions and privacy/keys twins answer Markdown with their headings', async () => {++ const companions = await t.get('/companions.md');++ assert.strictEqual(companions.status, 200);++ assert.match(companions.headers.get('content-type'), /^text\/markdown/);++ assert.ok(companions.text.includes('# Characters'), 'the gallery twin names the page heading');++++ const keys = await t.get('/privacy/keys.md');++ assert.strictEqual(keys.status, 200);++ assert.match(keys.headers.get('content-type'), /^text\/markdown/);++ assert.ok(keys.text.includes('# Your keys and accounts'), 'the keys twin names the page heading');++ assert.ok(keys.text.includes('Sign in'), 'signed out, it says what the page says to a signed-out visitor');++ assert.ok(!/api key|session cookie value|secret/i.test(keys.text), 'and nothing of an account credential');++++ // The owner-only console has no public page, so it has no twin and no llms.txt link.++ assert.strictEqual((await t.get('/console.md')).status, 404);++ const llms = await t.get('/llms.txt');++ assert.ok(llms.text.includes('[The gallery](https://openvibe.exchange/companions.md)'));++ assert.ok(llms.text.includes('[Your keys and accounts](https://openvibe.exchange/privacy/keys.md)'));++ assert.ok(!llms.text.includes('/console'));++ });++await check('/.well-known/security.txt is not swallowed by the layer', async () => {const r = await t.get('/.well-known/security.txt');assert.notStrictEqual(r.status, 200, 'the layer only takes agent.json');
@@ -3,6 +3,7 @@
@@ -3,6 +3,7 @@
// own render through the same views and layout the HTML routes use, always signed out: a chat, a try box and the
// own render through the same views and layout the HTML routes use, always signed out: a chat, a try box and the
// 18+ confirmation never appear in a twin.
// 18+ confirmation never appear in a twin.
const views = require('./views/exchange');
const views = require('./views/exchange');
+const companionViews = require('./views/companions');
const arenaViews = require('./views/arena');
const arenaViews = require('./views/arena');
const { CATEGORIES } = require('./arena/category');
const { CATEGORIES } = require('./arena/category');
const { ARENA_KIND } = require('./bench');
const { ARENA_KIND } = require('./bench');
@@ -13,7 +14,7 @@ const { homeJsonLd } = require('./http/discovery');
@@ -13,7 +14,7 @@ const { homeJsonLd } = require('./http/discovery');
const HOME_DESCRIPTION = 'OpenVibe.Exchange: every AI model, one account. Live prices, speed and uptime of every offer, from the families on sale today and more as their providers join; chat with one model or three side by side; one OpenAI- and Anthropic-compatible endpoint.';
const HOME_DESCRIPTION = 'OpenVibe.Exchange: every AI model, one account. Live prices, speed and uptime of every offer, from the families on sale today and more as their providers join; chat with one model or three side by side; one OpenAI- and Anthropic-compatible endpoint.';
function createAgentPages(ctx) {
function createAgentPages(ctx) {
- const { config, ai, bench, history } = ctx;
+ const { config, ai, bench, history, characters } = ctx;
const page = (path, o) => ({ html: renderPage({ viewer: { kind: 'anonymous' }, config, path, agentLayer: true, ...o }), title: o.title });
const page = (path, o) => ({ html: renderPage({ viewer: { kind: 'anonymous' }, config, path, agentLayer: true, ...o }), title: o.title });
async function home(path) {
async function home(path) {
@@ -83,6 +84,30 @@ function createAgentPages(ctx) {
@@ -83,6 +84,30 @@ function createAgentPages(ctx) {
]);
]);
return page(p, { index: true, title: 'Status', description: 'Uptime per offer, the model changelog, and incidents.', body: raw(views.statusPage({ catalog, usage, changes })) });
return page(p, { index: true, title: 'Status', description: 'Uptime per offer, the model changelog, and incidents.', body: raw(views.statusPage({ catalog, usage, changes })) });
}
}
+ if (p === '/companions') {
+ // The public gallery, always the signed-out view: no "Your characters", no adult cards.
+ const [gallery, tagList] = await Promise.all([
+ characters.gallery({ sort: 'new', q: '', tags: [], limit: 48, allowAdult: false }),
+ characters.allTags(24),
+ ]);
+ return page(p, {
+ index: true, title: 'Characters',
+ description: 'Character cards on OpenVibe.Exchange: bring a card in as Character Card V2 or V3, talk to it, and take it with you again.',
+ body: raw(companionViews.galleryPage({
+ items: gallery.items, total: gallery.total, q: '', tags: [], sort: 'new', tagList,
+ viewer: { kind: 'anonymous' }, adult: false, mine: [], defaults: '',
+ })),
+ });
+ }
+ if (p === '/privacy/keys') {
+ // Signed out, /privacy/keys only sends a person to sign in; the twin says that and nothing of an
+ // account, a credential or a key.
+ return page(p, {
+ index: false, title: 'Your keys and accounts',
+ description: 'Sign in to manage your own provider keys and supported browser session cookies.',
+ body: raw('<div class="ex-wrap ex-narrow"><header class="ex-companions-head"><h1>Your keys and accounts</h1><p class="ex-lede">Sign in to manage your own provider keys and supported browser session cookies.</p><p class="ex-head-actions"><a class="ovk-btn ovk-btn--primary" href="/auth/login?next=%2Fprivacy%2Fkeys">Sign in with OpenVibe</a></p></header></div>'),
+ });
+ }
return null;
return null;
}
}
@@ -127,7 +127,6 @@ function createDiscoveryRoutes(ctx) {
@@ -127,7 +127,6 @@ function createDiscoveryRoutes(ctx) {
{ title: 'Full text for language models', url: abs('/llms-full.txt') },
{ title: 'Full text for language models', url: abs('/llms-full.txt') },
{ title: 'Release metadata (JSON)', url: abs('/release.json') },
{ title: 'Release metadata (JSON)', url: abs('/release.json') },
{ title: 'AI router API', url: 'https://ai.openvibe.services', note: 'the API and run receipts' },
{ title: 'AI router API', url: 'https://ai.openvibe.services', note: 'the API and run receipts' },
- { title: 'AI operator console', url: abs('/console'), note: 'owner-only controls for providers, models, routes, quotas, usage and runs' },
{ title: 'Your keys and accounts', url: abs('/privacy/keys'), note: 'manage your own provider keys and supported browser session cookies' },
{ title: 'Your keys and accounts', url: abs('/privacy/keys'), note: 'manage your own provider keys and supported browser session cookies' },
{ title: 'OpenVibe Link', url: 'https://openvibe.link', note: 'offer a machine to the network' },
{ title: 'OpenVibe Link', url: 'https://openvibe.link', note: 'offer a machine to the network' },
],
],
@@ -49,6 +49,27 @@ const { createFakeAi } = require('./helpers/ai');
@@ -49,6 +49,27 @@ const { createFakeAi } = require('./helpers/ai');
assert.strictEqual((await t.get('/no-such-page.md')).status, 404);
assert.strictEqual((await t.get('/no-such-page.md')).status, 404);
});
});
+ await check('the companions and privacy/keys twins answer Markdown with their headings', async () => {
+ const companions = await t.get('/companions.md');
+ assert.strictEqual(companions.status, 200);
+ assert.match(companions.headers.get('content-type'), /^text\/markdown/);
+ assert.ok(companions.text.includes('# Characters'), 'the gallery twin names the page heading');
+
+ const keys = await t.get('/privacy/keys.md');
+ assert.strictEqual(keys.status, 200);
+ assert.match(keys.headers.get('content-type'), /^text\/markdown/);
+ assert.ok(keys.text.includes('# Your keys and accounts'), 'the keys twin names the page heading');
+ assert.ok(keys.text.includes('Sign in'), 'signed out, it says what the page says to a signed-out visitor');
+ assert.ok(!/api key|session cookie value|secret/i.test(keys.text), 'and nothing of an account credential');
+
+ // The owner-only console has no public page, so it has no twin and no llms.txt link.
+ assert.strictEqual((await t.get('/console.md')).status, 404);
+ const llms = await t.get('/llms.txt');
+ assert.ok(llms.text.includes('[The gallery](https://openvibe.exchange/companions.md)'));
+ assert.ok(llms.text.includes('[Your keys and accounts](https://openvibe.exchange/privacy/keys.md)'));
+ assert.ok(!llms.text.includes('/console'));
+ });
+
await check('/.well-known/security.txt is not swallowed by the layer', async () => {
await check('/.well-known/security.txt is not swallowed by the layer', async () => {
const r = await t.get('/.well-known/security.txt');
const r = await t.get('/.well-known/security.txt');
assert.notStrictEqual(r.status, 200, 'the layer only takes agent.json');
assert.notStrictEqual(r.status, 200, 'the layer only takes agent.json');