openvibers/openvibe.network Commits
Frame: the Sources popover's provider offers (#860)
Changes
@@@ -1023,6 +1023,29 @@ async function createFrameService(db, config, analytics, { privateKey = null, is// /api/v1/runs/:id/events (mounted in server/index.js), replayable from Last-Event-ID.router.post('/ask', ...ask.ask);++ // GET /api/frame/query/providers — the Sources popover's web ladder: Search's offers (provider, scopes, price,++ // availability, today's use), asked through Network for the same connect-src reason as /query. Same CORS and++ // caching: a person's token goes along and nothing is kept; signed out the offers are kept 60 s.++ const providerOffers = new Map();++ router.get('/query/providers', async (req, res) => {++ res.set({ 'Content-Type': 'application/json; charset=utf-8', Vary: 'Authorization, Origin' });++ const auth = /^Bearer [A-Za-z0-9._~+/=-]{20,4096}$/.test(String(req.headers.authorization || '')) ? String(req.headers.authorization) : '';++ const u = new URL('/api/v1/query/providers', svcUrl('search', 'http://127.0.0.1:4710'));++ const headers = { ...(req.ip ? { 'X-Forwarded-For': req.ip } : {}), ...(auth ? { Authorization: auth } : {}) };++ if (auth) {++ res.set('Cache-Control', 'private, no-store');++ const d = await getJson(u.href, headers, 10000);++ return res.send(JSON.stringify(d || { providers: [] }));++ }++ res.set('Cache-Control', 'public, max-age=60');++ const kept = providerOffers.get('offers');++ if (kept && Date.now() - kept.at < 60_000) return res.send(kept.body);++ const d = await getJson(u.href, headers, 10000);++ const body = JSON.stringify(d || { providers: [] });++ providerOffers.set('offers', { at: Date.now(), body });++ res.send(body);++ });++// POST /api/frame/hit — one anonymous count for the calling page's host. The host comes from the// browser-set Origin header (never the body) and must be one of ours or a registered tool domain.const recent = new Map(); // ip → [count, windowStart]: 40/min is plenty for a person, useless for stuffing@@@ -14,6 +14,7 @@ const { createFrameService } = require('../server/frame/service');asked.push({ url: req.url, auth: req.headers.authorization || '', xff: req.headers['x-forwarded-for'] });if (down) { res.statusCode = 503; return res.end('{}'); }res.setHeader('content-type', 'application/json');++ if (req.url.startsWith('/api/v1/query/providers')) return res.end(JSON.stringify({ providers: [{ provider: 'deepseek', scopes: ['web'], available: true }] }));res.end(JSON.stringify({ results: [{ scope: 'network', kind: 'document', title: 'surf_utopia', url: 'https://openvibe.live/clips/1' }], groups: { network: 1 }, answer: null, citations: [], route: { explain: [], cost_usd: 0 }, next_cursor: null }));});await new Promise((r) => search.listen(0, '127.0.0.1', r));@@@ -60,6 +61,19 @@ const { createFrameService } = require('../server/frame/service');assert.strictEqual(r.status, 200);assert.deepStrictEqual([d.results, d.next_cursor], [[], null], 'Search not answering is an empty answer');++ // The Sources popover's own read: Search's web ladder offers, same CORS and caching.++ down = false;++ let prov = await fetch(`${base.replace('/query', '/query/providers')}`).then((x) => x.json());++ assert.deepStrictEqual(prov.providers.map((p) => p.provider), ['deepseek']);++ const providersHit = asked[asked.length - 1];++ assert.strictEqual(new URL(providersHit.url, 'http://x').pathname, '/api/v1/query/providers');++ assert.strictEqual(providersHit.auth, '', 'anonymous providers');++ await fetch(`${base.replace('/query', '/query/providers')}`);++ assert.strictEqual(asked.filter((x) => x.url.startsWith('/api/v1/query/providers')).length, 1, 'the anonymous offers are kept 60 s');++ const provAuth = await fetch(`${base.replace('/query', '/query/providers')}`, { headers: { Authorization: token } });++ assert.strictEqual(provAuth.headers.get('cache-control'), 'private, no-store');++ assert.strictEqual(asked[asked.length - 1].auth, token, "the person's token goes along");++server.close(); search.close();console.log('frame query: the fabric through Network, as the person or kept 60 s ok');process.exit(0);
@@ -1023,6 +1023,29 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -1023,6 +1023,29 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
// /api/v1/runs/:id/events (mounted in server/index.js), replayable from Last-Event-ID.
// /api/v1/runs/:id/events (mounted in server/index.js), replayable from Last-Event-ID.
router.post('/ask', ...ask.ask);
router.post('/ask', ...ask.ask);
+ // GET /api/frame/query/providers — the Sources popover's web ladder: Search's offers (provider, scopes, price,
+ // availability, today's use), asked through Network for the same connect-src reason as /query. Same CORS and
+ // caching: a person's token goes along and nothing is kept; signed out the offers are kept 60 s.
+ const providerOffers = new Map();
+ router.get('/query/providers', async (req, res) => {
+ res.set({ 'Content-Type': 'application/json; charset=utf-8', Vary: 'Authorization, Origin' });
+ const auth = /^Bearer [A-Za-z0-9._~+/=-]{20,4096}$/.test(String(req.headers.authorization || '')) ? String(req.headers.authorization) : '';
+ const u = new URL('/api/v1/query/providers', svcUrl('search', 'http://127.0.0.1:4710'));
+ const headers = { ...(req.ip ? { 'X-Forwarded-For': req.ip } : {}), ...(auth ? { Authorization: auth } : {}) };
+ if (auth) {
+ res.set('Cache-Control', 'private, no-store');
+ const d = await getJson(u.href, headers, 10000);
+ return res.send(JSON.stringify(d || { providers: [] }));
+ }
+ res.set('Cache-Control', 'public, max-age=60');
+ const kept = providerOffers.get('offers');
+ if (kept && Date.now() - kept.at < 60_000) return res.send(kept.body);
+ const d = await getJson(u.href, headers, 10000);
+ const body = JSON.stringify(d || { providers: [] });
+ providerOffers.set('offers', { at: Date.now(), body });
+ res.send(body);
+ });
+
// POST /api/frame/hit — one anonymous count for the calling page's host. The host comes from the
// POST /api/frame/hit — one anonymous count for the calling page's host. The host comes from the
// browser-set Origin header (never the body) and must be one of ours or a registered tool domain.
// browser-set Origin header (never the body) and must be one of ours or a registered tool domain.
const recent = new Map(); // ip → [count, windowStart]: 40/min is plenty for a person, useless for stuffing
const recent = new Map(); // ip → [count, windowStart]: 40/min is plenty for a person, useless for stuffing
@@ -14,6 +14,7 @@ const { createFrameService } = require('../server/frame/service');
@@ -14,6 +14,7 @@ const { createFrameService } = require('../server/frame/service');
asked.push({ url: req.url, auth: req.headers.authorization || '', xff: req.headers['x-forwarded-for'] });
asked.push({ url: req.url, auth: req.headers.authorization || '', xff: req.headers['x-forwarded-for'] });
if (down) { res.statusCode = 503; return res.end('{}'); }
if (down) { res.statusCode = 503; return res.end('{}'); }
res.setHeader('content-type', 'application/json');
res.setHeader('content-type', 'application/json');
+ if (req.url.startsWith('/api/v1/query/providers')) return res.end(JSON.stringify({ providers: [{ provider: 'deepseek', scopes: ['web'], available: true }] }));
res.end(JSON.stringify({ results: [{ scope: 'network', kind: 'document', title: 'surf_utopia', url: 'https://openvibe.live/clips/1' }], groups: { network: 1 }, answer: null, citations: [], route: { explain: [], cost_usd: 0 }, next_cursor: null }));
res.end(JSON.stringify({ results: [{ scope: 'network', kind: 'document', title: 'surf_utopia', url: 'https://openvibe.live/clips/1' }], groups: { network: 1 }, answer: null, citations: [], route: { explain: [], cost_usd: 0 }, next_cursor: null }));
});
});
await new Promise((r) => search.listen(0, '127.0.0.1', r));
await new Promise((r) => search.listen(0, '127.0.0.1', r));
@@ -60,6 +61,19 @@ const { createFrameService } = require('../server/frame/service');
@@ -60,6 +61,19 @@ const { createFrameService } = require('../server/frame/service');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.deepStrictEqual([d.results, d.next_cursor], [[], null], 'Search not answering is an empty answer');
assert.deepStrictEqual([d.results, d.next_cursor], [[], null], 'Search not answering is an empty answer');
+ // The Sources popover's own read: Search's web ladder offers, same CORS and caching.
+ down = false;
+ let prov = await fetch(`${base.replace('/query', '/query/providers')}`).then((x) => x.json());
+ assert.deepStrictEqual(prov.providers.map((p) => p.provider), ['deepseek']);
+ const providersHit = asked[asked.length - 1];
+ assert.strictEqual(new URL(providersHit.url, 'http://x').pathname, '/api/v1/query/providers');
+ assert.strictEqual(providersHit.auth, '', 'anonymous providers');
+ await fetch(`${base.replace('/query', '/query/providers')}`);
+ assert.strictEqual(asked.filter((x) => x.url.startsWith('/api/v1/query/providers')).length, 1, 'the anonymous offers are kept 60 s');
+ const provAuth = await fetch(`${base.replace('/query', '/query/providers')}`, { headers: { Authorization: token } });
+ assert.strictEqual(provAuth.headers.get('cache-control'), 'private, no-store');
+ assert.strictEqual(asked[asked.length - 1].auth, token, "the person's token goes along");
+
server.close(); search.close();
server.close(); search.close();
console.log('frame query: the fabric through Network, as the person or kept 60 s ok');
console.log('frame query: the fabric through Network, as the person or kept 60 s ok');
process.exit(0);
process.exit(0);