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 }; }, }); }