88 lines
6.0 KiB
JavaScript
88 lines
6.0 KiB
JavaScript
// Row 208: isolated real broker + chat HTTP; no config, slot, provider or live host.
|
|
import { createServer, request } from 'node:http';
|
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { Broker } from '../../bus/src/broker.mjs';
|
|
import { Store } from '../../bus/src/store.mjs';
|
|
import { createChat } from '../src/chat.mjs';
|
|
import { chatHTTP } from '../src/chat-http.mjs';
|
|
import { consoleAccess } from '../src/access.mjs';
|
|
import { BusReadError } from '../src/bus.mjs';
|
|
|
|
export const mutants = {
|
|
composer: ["${threadWrites() ? `<form id=\"chat-send-form\"", "${true ? `<form id=\"chat-send-form\""],
|
|
open: ['${threadWrites() ? `<button class="btn" type="button" id="chat-resume"', '${true ? `<button class="btn" type="button" id="chat-resume"'],
|
|
share: ["if (!current.canSend || !threadWrites() || current.tester) return '';", "if (!current.canSend || current.tester) return '';"],
|
|
create: ["if (current.user && (project || !scopeWrites(key))) return '';", "if (current.user && project) return '';"],
|
|
};
|
|
export async function sharingFixture(t, { owner = false, mutant = process.env.POE_UI_MUTANT } = {}) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'poe-sharing-'));
|
|
const store = new Store(dir), calls = [], lines = [];
|
|
const access = { accessVersion: 1, revision: 1, users: [
|
|
{ id: 'alice', home: 'local', subjects: [{ provider: 'fake', sub: 'alice-sub' }] },
|
|
{ id: 'bob', home: 'local', subjects: [{ provider: 'fake', sub: 'bob-sub' }] },
|
|
], grants: ['alice', 'bob'].map(user => ({ id: 'g-' + user, user, business: 'demo', projects: ['*'], instances: ['assistant'], level: 'collaborator' })) };
|
|
const broker = new Broker({ store, access: () => access, businesses: { demo: {
|
|
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
|
|
roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } }, assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } } },
|
|
definitions: { 'chief-of-staff': {}, assistant: { messaging: JSON.parse(readFileSync(new URL('../../../roles/assistant.json', import.meta.url))).messaging } },
|
|
} } });
|
|
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
|
const invoke = (cap, verb, args = {}) => broker.request(cap, { verb, args });
|
|
const alice = () => broker.bindUser({ business: 'demo', user: 'alice' });
|
|
const bob = () => broker.bindUser({ business: 'demo', user: 'bob' });
|
|
const project = invoke(human, 'project.create', { name: 'Viewer project' }).id;
|
|
const other = invoke(human, 'project.create', { name: 'Collaborator project' }).id;
|
|
const own = invoke(alice(), 'thread.create', { project, instance: 'assistant', name: 'Old own thread' }).id;
|
|
const writable = invoke(alice(), 'thread.create', { project: other, instance: 'assistant', name: 'Writable thread' }).id;
|
|
const shared = invoke(bob(), 'thread.create', { project, instance: 'assistant', name: 'Shared thread <img src=x onerror=alert(1)>' }).id;
|
|
invoke(alice(), 'thread.send', { thread: own, body: 'Own history' });
|
|
invoke(bob(), 'thread.send', { thread: shared, body: 'Shared history' });
|
|
invoke(bob(), 'thread.share', { thread: shared, to: 'user:alice' });
|
|
access.grants[0].level = 'viewer';
|
|
const hints = consoleAccess({ load: () => access, business: () => 'demo' });
|
|
const principal = () => owner ? human : alice();
|
|
const chat = createChat({ call: async (verb, args) => {
|
|
calls.push([verb, args]);
|
|
if (verb === 'plugins') return { plugins: { channel: ['webui'] } };
|
|
try { return invoke(principal(), verb, args); }
|
|
catch (e) { throw new BusReadError(e.code); }
|
|
} });
|
|
const source = readFileSync(new URL('../src/public/chat.js', import.meta.url), 'utf8');
|
|
let chatSource = source;
|
|
if (mutant) {
|
|
const [before, after] = mutants[mutant] ?? [];
|
|
if (!before || !source.includes(before)) throw Error('mutant anchor missing: ' + mutant);
|
|
chatSource = source.replace(before, after);
|
|
}
|
|
const server = createServer(async (req, res) => {
|
|
const url = new URL(req.url, 'http://fixture');
|
|
if (url.pathname === '/api/config') {
|
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
return res.end(JSON.stringify({ send: true, user: !owner, chatGrants: hints.chatGrants({ user: 'alice', provider: 'fake', sub: 'alice-sub' }) }));
|
|
}
|
|
if (url.pathname.startsWith('/api/chat/')) return chatHTTP(req, res, {
|
|
path: url.pathname, search: url.search, chat, named: true, send: true,
|
|
preview: { sso: true, user: owner ? 'jason' : 'alice', origin: origin() }, log: line => lines.push(line),
|
|
});
|
|
if (url.pathname === '/chat.js') { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(chatSource); }
|
|
if (url.pathname === '/') {
|
|
res.writeHead(200, { 'content-type': 'text/html' });
|
|
return res.end('<!doctype html><title>Sharing fixture</title><div id="view"></div><script src="/chat.js"></script>');
|
|
}
|
|
res.writeHead(404); res.end();
|
|
});
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
const origin = () => `http://127.0.0.1:${server.address().port}`;
|
|
t.after(async () => { await new Promise(r => server.close(r)); store.close(); rmSync(dir, { recursive: true, force: true }); });
|
|
const http = (path, value, extra = {}) => new Promise((resolve, reject) => {
|
|
const body = value === undefined ? undefined : JSON.stringify(value);
|
|
const req = request(origin() + path, { method: body ? 'POST' : 'GET', headers: body ? { origin: origin(), 'sec-fetch-site': 'same-origin', 'content-type': 'application/json', ...extra } : extra }, res => {
|
|
let data = ''; res.on('data', chunk => { data += chunk; });
|
|
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data) }));
|
|
}); req.on('error', reject); req.end(body);
|
|
});
|
|
return { store, access, broker, human, alice, bob, invoke, project, other, own, writable, shared, calls, lines, origin: origin(), http, hints, source, chatSource };
|
|
}
|