Files

80 lines
4.3 KiB
JavaScript

import { plainMessage } from './org-admin.mjs';
// The registry and service are injected here so their single, shared implementations remain
// the authority. The browser supplies only an id; roots and sockets come from verified metadata.
// Only the service's own coded messages are shown, never parser text, which can quote file bytes.
function publicReason(ctx, error) {
if (typeof error?.code !== 'string' || typeof error.message !== 'string') return 'organization could not be read';
return plainMessage(ctx, error.message.replace(/^(invalid registry):[\s\S]*$/, '$1'));
}
export function organizationConsole({ ctx, listOrgs, showOrg, resolveOrg, orgPaths, busStatus, options }) {
async function admitted(id, session) {
const principal = { provider: session?.provider, iss: session?.iss, sub: session?.sub };
if (Object.values(principal).some(value => typeof value !== 'string' || !value)) throw new Error('org-unavailable');
const context = { ...ctx, principal };
const shown = await showOrg(context, id);
const org = await resolveOrg(ctx, id);
if (org.state !== 'active') throw new Error('org-unavailable');
return { org, shown, context };
}
async function list(session) {
const entries = await listOrgs(ctx), result = [];
for (const entry of entries) {
if (entry.state !== 'active') continue;
let verified;
try { verified = await admitted(entry.id, session); }
catch (error) {
// Show a broken org only to its verified owner (the service marks failures after the
// owner check). Anything else, including a broken root or owner file, stays hidden.
if (error?.ownerVerified !== true) continue;
result.push({ id: entry.id, label: entry.id, invalid: true, reason: publicReason(ctx, error) });
continue;
}
let busHealthy = false;
try { busHealthy = (await busStatus(ctx, entry.id)).healthy === true; } catch { /* offline metadata remains visible */ }
result.push({ id: entry.id, label: entry.id, theme: verified.shown.doc?.appearance?.theme ?? null, busHealthy });
}
return result;
}
// U1 (#1873): the orgs a session belongs to, for the cross-organization read (workspace-read.mjs).
// The same admission as `list` and `resolve`: an org the session does not own is skipped, and a
// broken org shows only to its verified owner, as invalid. Bus status and options are lazy, so a
// stopped bus never gets a transport; `timeoutMs` bounds each bus read the options make.
async function members(session, { timeoutMs } = {}) {
const entries = await listOrgs(ctx), result = [];
for (const entry of entries) {
if (entry.state !== 'active') continue;
let verified;
try { verified = await admitted(entry.id, session); }
catch (error) {
if (error?.ownerVerified !== true) continue;
result.push({ id: entry.id, invalid: true, reason: publicReason(ctx, error) });
continue;
}
const { org, shown, context } = verified, id = entry.id;
result.push({
id, doc: shown.doc,
// Host paths in a refusal or boot reason become relative to the instance before they leave.
async status(bound) {
let s;
try { s = await busStatus(ctx, id, bound); }
catch (error) { throw new Error(plainMessage(ctx, typeof error?.message === 'string' ? error.message : 'bus state could not be read')); }
const boot = s?.boot && typeof s.boot === 'object' ? { ok: s.boot.ok, at: s.boot.at, lastGood: s.boot.lastGood ?? null, reason: s.boot.reason == null ? null : plainMessage(ctx, s.boot.reason) } : null;
return { healthy: s?.healthy === true, pid: Number.isSafeInteger(s?.pid) ? s.pid : null, boot };
},
options: bound => options({ id, root: org.root, paths: orgPaths(org.root), session, context, doc: shown.doc, ...(timeoutMs === undefined ? {} : { timeoutMs }), ...bound }),
});
}
return result;
}
return Object.freeze({
list,
members,
async resolve(id, session) {
const { org, shown, context } = await admitted(id, session);
let busHealthy = false;
try { busHealthy = (await busStatus(ctx, id)).healthy === true; } catch { /* unavailable */ }
return { ...options({ id, root: org.root, paths: orgPaths(org.root), session, context, doc: shown.doc }), busHealthy };
},
});
}