Mirror orbit.server into the sandbox via the state snapshot

This commit is contained in:
Jean Chevronnet 2026-07-06 17:52:06 +00:00
parent 71dd987fb5
commit fa115e5bc3
4 changed files with 30 additions and 5 deletions

View file

@ -45,8 +45,11 @@ The sandboxed API (`src/modules/sandbox/host.ts` + `public/plugin-sandbox.html`)
`orbit.log`, `orbit.on(event, fn)` (forwarded `connected` / `message` /
`buffer.active` / `status`), `orbit.irc.*`, `orbit.notify`,
`orbit.state.active/nick/account/buffers` (from a pushed snapshot, synchronous),
`orbit.storage.get/set`, and `orbit.ui(slot, build)` where `build(el)` populates the
plugin's own iframe and the host sizes the frame to the content.
`orbit.server.network/isupport/hasCap/caps` (also from the snapshot, synchronous —
gate cap-dependent behaviour so a plugin never 421s a lean server; no `numeric()`
here since the sandbox gets no `raw` events), `orbit.storage.get/set`, and
`orbit.ui(slot, build)` where `build(el)` populates the plugin's own iframe and the
host sizes the frame to the content.
The app's theme CSS vars (`--bg`, `--ink`, `--accent`, `--muted`, `--border`,
`--panel`, `--green-soft`) are mirrored into the sandbox and kept in sync on theme

View file

@ -22,7 +22,7 @@
* served with its own strict CSP.
*/
(function () {
var port, snap = { active: '', nick: '', account: '', buffers: [] }, store = {};
var port, snap = { active: '', nick: '', account: '', buffers: [], network: '', isupport: {}, caps: [] }, store = {};
var listeners = {}; // event name -> [fn]
var pending = new Map(), rpcId = 0; // in-flight RPCs
@ -87,6 +87,13 @@
account: function () { return snap.account; },
buffers: function () { return snap.buffers.slice(); },
},
// Read-only server / capability info, from the snapshot (no RPC, no grant).
server: {
network: function () { return snap.network || ''; },
isupport: function () { var o = {}, m = snap.isupport || {}, k; for (k in m) o[k] = m[k]; return o; },
hasCap: function (cap) { return (snap.caps || []).some(function (c) { return c.name === cap && c.enabled; }); },
caps: function () { return (snap.caps || []).map(function (c) { return { name: c.name, available: c.available, enabled: c.enabled }; }); },
},
storage: {
get: function (k, fallback) { return k in store ? store[k] : fallback; },
set: function (k, v) { store[k] = v; return rpc('storage.set', k, v); },

View file

@ -26,7 +26,13 @@ const STORE_PREFIX = 'orbit-sbx:';
function snapshot(): StateSnapshot {
const s = activeStore().getState();
return { active: s.active, nick: s.nick, account: s.account, buffers: Object.keys(s.buffers) };
const cl = s.client;
return {
active: s.active, nick: s.nick, account: s.account, buffers: Object.keys(s.buffers),
network: cl?.server.network ?? '',
isupport: { ...(cl?.server.isupport ?? {}) },
caps: cl?.ircv3.listCaps() ?? [],
};
}
// Current values of the app's themeable CSS vars, mirrored into the sandbox.

View file

@ -70,7 +70,16 @@ export const THEME_VARS = ['--bg', '--ink', '--accent', '--muted', '--border', '
// guest -> host
export interface RpcMsg { type: 'rpc'; id: number; method: string; args: unknown[]; }
export interface StateSnapshot { active: string; nick: string; account: string; buffers: string[]; }
export interface StateSnapshot {
active: string; nick: string; account: string; buffers: string[];
// Read-only server / capability info (client.server + client.ircv3), pushed in the
// snapshot so a sandboxed plugin can gate cap-dependent behaviour synchronously —
// never a raw command that 421s a lean server. (No numeric map: the sandbox gets
// no 'raw' events, so there are no numeric replies to name.)
network: string;
isupport: Record<string, string>;
caps: { name: string; available: boolean; enabled: boolean }[];
}
export type HostToGuest = InitMsg | EventMsg | SnapshotMsg | ThemeMsg | RpcReplyMsg;
export type GuestToHost = RpcMsg;