openvibers/openvibe.tools Commits
Guest conversion for the launcher (WS-B task 8): the first time an account asks for its recent tools from a browser, the tools that browser used as a guest (ov_recent_tools) join the account's tools.usage list, once per account and browser (HttpOnly ov_recent_merged marker)
Changes
@@@ -77,6 +77,22 @@ const fetchImpl = async (url, opts) => {mirror(req('yaml.example.org', '/', { 'sec-fetch-dest': 'document' }, null), { append: (k, v) => cookies.push(v) }, () => {});assert.strictEqual(cookies.length, 0, 'no zone cookie from a custom domain');++ // Guest conversion (WS-B task 8): the guest's tools join the account once per account and browser.++ const { mergeGuestTools, MERGED_COOKIE } = require('../usage');++ const recorded = [];++ const fake = { enabled: true, record: (s, tool) => { recorded.push([s, tool]); return true; } };++ const set = [];++ const resOf = () => ({ append: (k, v) => set.push(v) });++ let added = mergeGuestTools({ headers: { cookie: 'ov_recent_tools=png.yaml.jsonminify' } }, resOf(), USR, [{ tool: 'yaml' }], ['png', 'yaml', 'jsonminify'], fake);++ assert.deepStrictEqual(added, ['png', 'jsonminify'], 'only what the account lacks, newest first');++ assert.deepStrictEqual(recorded, [[USR, 'jsonminify'], [USR, 'png']], 'recorded oldest first so the newest ends on top');++ assert.ok(set[0].startsWith(`${MERGED_COOKIE}=${USR};`) && /HttpOnly/.test(set[0]) && /Domain=\.openvibe\.tools/.test(set[0]));++ added = mergeGuestTools({ headers: { cookie: `ov_recent_tools=png; ${MERGED_COOKIE}=${USR}` } }, resOf(), USR, [], ['png'], fake);++ assert.deepStrictEqual(added, [], 'merged once per account and browser');++ assert.deepStrictEqual(mergeGuestTools({ headers: { cookie: `${MERGED_COOKIE}=${USR}` } }, resOf(), 'usr_01JAB2C3D4E5F6G7H8J9K0OTHR', [], ['png'], fake), ['png'], 'another account on the same browser gets them too');++ assert.deepStrictEqual(mergeGuestTools({ headers: {} }, resOf(), USR, [], [], fake), [], 'nothing to merge');++ assert.deepStrictEqual(mergeGuestTools({ headers: {} }, resOf(), USR, [], ['png'], { enabled: false }), [], 'recorder off');++const off = createRecorder({ env: {}, fetchImpl });assert.strictEqual(off.enabled, false);assert.strictEqual(off.record(USR, 'yaml'), false, 'off without a service secret');@@@ -132,6 +132,24 @@ function rememberInCookie(req, res, tool, host) {let _recorder = null;function recorder() { if (!_recorder) _recorder = createRecorder(); return _recorder; }++const MERGED_COOKIE = 'ov_recent_merged';++/**++ * Guest conversion (roadmap WS-B task 8): tools this browser used before the person signed in join++ * their account's list, once per account and browser (a marker cookie names the subject merged).++ * Returns the tools added (newest first); records them through the recorder.++ */++function mergeGuestTools(req, res, subject, accountRecent, fromCookie, rec = recorder()) {++ if (!subject || !rec.enabled || !fromCookie.length) return [];++ const raw = String(req.headers.cookie || '');++ const m = new RegExp(`(?:^|;\\s*)${MERGED_COOKIE}=([^;]*)`).exec(raw);++ if (m && decodeURIComponent(m[1]) === subject) return [];++ const have = new Set((accountRecent || []).map((e) => e && e.tool));++ const add = fromCookie.filter((t) => !have.has(t));++ for (const tool of [...add].reverse()) rec.record(subject, tool);++ res.append('Set-Cookie', `${MERGED_COOKIE}=${encodeURIComponent(subject)}; Domain=.openvibe.tools; Path=/; Max-Age=${365 * 24 * 3600}; SameSite=Lax; Secure; HttpOnly`);++ return add;++}++/*** Middleware: a GET of a tool's page by a signed-in person (req.user.subject_id from guard.identify)* records that tool. The host names the tool (the registry's hosts, or the X-OV-Tool the gateway sends).@@@ -169,4 +187,4 @@ function usagePages({ snapshot, rec = null }) {};}--module.exports = { createRecorder, recorder, usagePages, recentFromCookie, NS, COOKIE };++module.exports = { createRecorder, recorder, usagePages, recentFromCookie, mergeGuestTools, NS, COOKIE, MERGED_COOKIE };@@@ -193,15 +193,22 @@ app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, max: 60, keyGenerator: (reqapp.use(createToolsApi({ snapshot: toolRegistry.snapshot }));// The launcher's recent tools: a signed-in person's tools.usage module (every device), otherwise this--// browser's ov_recent_tools cookie. Only tools the registry still lists; never cached.++// browser's ov_recent_tools cookie. Only tools the registry still lists; never cached. Guest conversion++// (WS-B task 8): the first time an account asks from a browser, the tools this browser used as a guest++// join the account's list (once per account and browser: ov_recent_merged).app.get('/api/v1/me/recent-tools', async (req, res) => {const usage = require('../../_shared/usage');res.set('Cache-Control', 'private, no-store');const known = new Set((toolRegistry.snapshot().tools || []).filter((t) => t.status !== 'unavailable').map((t) => t.id));const sid = req.user && req.user.subject_id;-- let mode = 'device', recent = usage.recentFromCookie(req).map((tool) => ({ tool }));++ const fromCookie = usage.recentFromCookie(req);++ let mode = 'device', recent = fromCookie.map((tool) => ({ tool }));if (sid && usage.recorder().enabled) {-- try { recent = await usage.recorder().recent(sid); mode = 'account'; } catch { /* the cookie list stands in */ }++ try {++ recent = await usage.recorder().recent(sid); mode = 'account';++ const merged = usage.mergeGuestTools(req, res, sid, recent, fromCookie);++ if (merged.length) recent = [...merged.map((tool) => ({ tool })), ...recent];++ } catch { /* the cookie list stands in */ }}res.json({ mode, recent: recent.filter((e) => e && known.has(e.tool)).slice(0, 12) });});
@@ -77,6 +77,22 @@ const fetchImpl = async (url, opts) => {
@@ -77,6 +77,22 @@ const fetchImpl = async (url, opts) => {
mirror(req('yaml.example.org', '/', { 'sec-fetch-dest': 'document' }, null), { append: (k, v) => cookies.push(v) }, () => {});
mirror(req('yaml.example.org', '/', { 'sec-fetch-dest': 'document' }, null), { append: (k, v) => cookies.push(v) }, () => {});
assert.strictEqual(cookies.length, 0, 'no zone cookie from a custom domain');
assert.strictEqual(cookies.length, 0, 'no zone cookie from a custom domain');
+ // Guest conversion (WS-B task 8): the guest's tools join the account once per account and browser.
+ const { mergeGuestTools, MERGED_COOKIE } = require('../usage');
+ const recorded = [];
+ const fake = { enabled: true, record: (s, tool) => { recorded.push([s, tool]); return true; } };
+ const set = [];
+ const resOf = () => ({ append: (k, v) => set.push(v) });
+ let added = mergeGuestTools({ headers: { cookie: 'ov_recent_tools=png.yaml.jsonminify' } }, resOf(), USR, [{ tool: 'yaml' }], ['png', 'yaml', 'jsonminify'], fake);
+ assert.deepStrictEqual(added, ['png', 'jsonminify'], 'only what the account lacks, newest first');
+ assert.deepStrictEqual(recorded, [[USR, 'jsonminify'], [USR, 'png']], 'recorded oldest first so the newest ends on top');
+ assert.ok(set[0].startsWith(`${MERGED_COOKIE}=${USR};`) && /HttpOnly/.test(set[0]) && /Domain=\.openvibe\.tools/.test(set[0]));
+ added = mergeGuestTools({ headers: { cookie: `ov_recent_tools=png; ${MERGED_COOKIE}=${USR}` } }, resOf(), USR, [], ['png'], fake);
+ assert.deepStrictEqual(added, [], 'merged once per account and browser');
+ assert.deepStrictEqual(mergeGuestTools({ headers: { cookie: `${MERGED_COOKIE}=${USR}` } }, resOf(), 'usr_01JAB2C3D4E5F6G7H8J9K0OTHR', [], ['png'], fake), ['png'], 'another account on the same browser gets them too');
+ assert.deepStrictEqual(mergeGuestTools({ headers: {} }, resOf(), USR, [], [], fake), [], 'nothing to merge');
+ assert.deepStrictEqual(mergeGuestTools({ headers: {} }, resOf(), USR, [], ['png'], { enabled: false }), [], 'recorder off');
+
const off = createRecorder({ env: {}, fetchImpl });
const off = createRecorder({ env: {}, fetchImpl });
assert.strictEqual(off.enabled, false);
assert.strictEqual(off.enabled, false);
assert.strictEqual(off.record(USR, 'yaml'), false, 'off without a service secret');
assert.strictEqual(off.record(USR, 'yaml'), false, 'off without a service secret');
@@ -132,6 +132,24 @@ function rememberInCookie(req, res, tool, host) {
@@ -132,6 +132,24 @@ function rememberInCookie(req, res, tool, host) {
let _recorder = null;
let _recorder = null;
function recorder() { if (!_recorder) _recorder = createRecorder(); return _recorder; }
function recorder() { if (!_recorder) _recorder = createRecorder(); return _recorder; }
+const MERGED_COOKIE = 'ov_recent_merged';
+/**
+ * Guest conversion (roadmap WS-B task 8): tools this browser used before the person signed in join
+ * their account's list, once per account and browser (a marker cookie names the subject merged).
+ * Returns the tools added (newest first); records them through the recorder.
+ */
+function mergeGuestTools(req, res, subject, accountRecent, fromCookie, rec = recorder()) {
+ if (!subject || !rec.enabled || !fromCookie.length) return [];
+ const raw = String(req.headers.cookie || '');
+ const m = new RegExp(`(?:^|;\\s*)${MERGED_COOKIE}=([^;]*)`).exec(raw);
+ if (m && decodeURIComponent(m[1]) === subject) return [];
+ const have = new Set((accountRecent || []).map((e) => e && e.tool));
+ const add = fromCookie.filter((t) => !have.has(t));
+ for (const tool of [...add].reverse()) rec.record(subject, tool);
+ res.append('Set-Cookie', `${MERGED_COOKIE}=${encodeURIComponent(subject)}; Domain=.openvibe.tools; Path=/; Max-Age=${365 * 24 * 3600}; SameSite=Lax; Secure; HttpOnly`);
+ return add;
+}
+
/**
/**
* Middleware: a GET of a tool's page by a signed-in person (req.user.subject_id from guard.identify)
* Middleware: a GET of a tool's page by a signed-in person (req.user.subject_id from guard.identify)
* records that tool. The host names the tool (the registry's hosts, or the X-OV-Tool the gateway sends).
* records that tool. The host names the tool (the registry's hosts, or the X-OV-Tool the gateway sends).
@@ -169,4 +187,4 @@ function usagePages({ snapshot, rec = null }) {
@@ -169,4 +187,4 @@ function usagePages({ snapshot, rec = null }) {
};
};
}
}
-module.exports = { createRecorder, recorder, usagePages, recentFromCookie, NS, COOKIE };
+module.exports = { createRecorder, recorder, usagePages, recentFromCookie, mergeGuestTools, NS, COOKIE, MERGED_COOKIE };
@@ -193,15 +193,22 @@ app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, max: 60, keyGenerator: (req
@@ -193,15 +193,22 @@ app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, max: 60, keyGenerator: (req
app.use(createToolsApi({ snapshot: toolRegistry.snapshot }));
app.use(createToolsApi({ snapshot: toolRegistry.snapshot }));
// The launcher's recent tools: a signed-in person's tools.usage module (every device), otherwise this
// The launcher's recent tools: a signed-in person's tools.usage module (every device), otherwise this
-// browser's ov_recent_tools cookie. Only tools the registry still lists; never cached.
+// browser's ov_recent_tools cookie. Only tools the registry still lists; never cached. Guest conversion
+// (WS-B task 8): the first time an account asks from a browser, the tools this browser used as a guest
+// join the account's list (once per account and browser: ov_recent_merged).
app.get('/api/v1/me/recent-tools', async (req, res) => {
app.get('/api/v1/me/recent-tools', async (req, res) => {
const usage = require('../../_shared/usage');
const usage = require('../../_shared/usage');
res.set('Cache-Control', 'private, no-store');
res.set('Cache-Control', 'private, no-store');
const known = new Set((toolRegistry.snapshot().tools || []).filter((t) => t.status !== 'unavailable').map((t) => t.id));
const known = new Set((toolRegistry.snapshot().tools || []).filter((t) => t.status !== 'unavailable').map((t) => t.id));
const sid = req.user && req.user.subject_id;
const sid = req.user && req.user.subject_id;
- let mode = 'device', recent = usage.recentFromCookie(req).map((tool) => ({ tool }));
+ const fromCookie = usage.recentFromCookie(req);
+ let mode = 'device', recent = fromCookie.map((tool) => ({ tool }));
if (sid && usage.recorder().enabled) {
if (sid && usage.recorder().enabled) {
- try { recent = await usage.recorder().recent(sid); mode = 'account'; } catch { /* the cookie list stands in */ }
+ try {
+ recent = await usage.recorder().recent(sid); mode = 'account';
+ const merged = usage.mergeGuestTools(req, res, sid, recent, fromCookie);
+ if (merged.length) recent = [...merged.map((tool) => ({ tool })), ...recent];
+ } catch { /* the cookie list stands in */ }
}
}
res.json({ mode, recent: recent.filter((e) => e && known.has(e.tool)).slice(0, 12) });
res.json({ mode, recent: recent.filter((e) => e && known.has(e.tool)).slice(0, 12) });
});
});