diff --git a/docs/SANDBOX.md b/docs/SANDBOX.md index c01dc4d..633b2f5 100644 --- a/docs/SANDBOX.md +++ b/docs/SANDBOX.md @@ -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 diff --git a/public/plugin-sandbox.html b/public/plugin-sandbox.html index b9f8673..45193c2 100644 --- a/public/plugin-sandbox.html +++ b/public/plugin-sandbox.html @@ -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); }, diff --git a/src/modules/sandbox/host.ts b/src/modules/sandbox/host.ts index c691344..d2b4be1 100644 --- a/src/modules/sandbox/host.ts +++ b/src/modules/sandbox/host.ts @@ -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. diff --git a/src/modules/sandbox/protocol.ts b/src/modules/sandbox/protocol.ts index 345a207..07a7580 100644 --- a/src/modules/sandbox/protocol.ts +++ b/src/modules/sandbox/protocol.ts @@ -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; + caps: { name: string; available: boolean; enabled: boolean }[]; +} export type HostToGuest = InitMsg | EventMsg | SnapshotMsg | ThemeMsg | RpcReplyMsg; export type GuestToHost = RpcMsg;