plugins: consolidate under third/, run the clock fully sandboxed

This commit is contained in:
Jean Chevronnet 2026-07-07 13:42:33 +00:00
parent cd8b7a9c70
commit 7bf51bdff8
10 changed files with 55 additions and 43 deletions

View file

@ -9,12 +9,28 @@ add UI in predefined slots.
> between releases. Plugins are deployment-controlled (same trust level as the
> app itself) — there is no user-uploaded plugin marketplace, by design.
## Where plugins live
Plugin files live under **`public/plugins/third/`** (served at `/app/plugins/third/`).
Third-party plugins **should be sandboxed** — run in an opaque-origin iframe reachable
only through the capability bridge (see [SANDBOX.md](./SANDBOX.md)). In-page loading
(full page access, no isolation) is reserved for trusted first-party code and is
being phased out.
## Enabling plugins
Add script URLs to `plugins` in [`config.json`](../CONFIG.md):
Add script URLs to `plugins` in [`config.json`](../CONFIG.md). Prefer the sandboxed
object form with an explicit permission list:
```json
{ "plugins": ["/app/plugins/orbit-demo.js"] }
{ "plugins": [{ "url": "/app/plugins/third/orbit-clock.js", "sandbox": true, "permissions": [] }] }
```
A bare URL (or an object without `"sandbox": true`) still loads **in-page** — trusted,
full access — for first-party plugins that need the React API:
```json
{ "plugins": ["/app/plugins/third/orbit-demo.js"] }
```
They load in order, after the app boots. Host them anywhere the page can reach
@ -143,6 +159,6 @@ a hook that isn't here.
| File | Shows |
|---|---|
| [`orbit-demo.js`](../public/plugins/orbit-demo.js) | events, a `composer_button`, an IRC action |
| [`orbit-clock.js`](../public/plugins/orbit-clock.js) | a `topbar_item` with live React-hook state |
| [`orbit-copy.js`](../public/plugins/orbit-copy.js) | a `message_action` (toolbar copy button with copied-confirmation) |
| [`orbit-demo.js`](../public/plugins/third/orbit-demo.js) | events, a `composer_button`, an IRC action |
| [`orbit-clock.js`](../public/plugins/third/orbit-clock.js) | a `topbar_item` with live React-hook state |
| [`orbit-copy.js`](../public/plugins/third/orbit-copy.js) | a `message_action` (toolbar copy button with copied-confirmation) |

View file

@ -18,10 +18,13 @@ Two things use it:
fetched and mounted the same way. Use for community / third-party / less-trusted
plugins. (Plain string entries still load in-page as a trusted `<script>`.)
Plugin files live under `public/plugins/third/` (served at `/app/plugins/third/`):
```jsonc
"plugins": [
"/app/plugins/orbit-clock.js", // in-page (trusted)
{ "url": "/app/plugins/community-thing.js", // sandboxed
{ "url": "/app/plugins/third/orbit-clock.js", // sandboxed, no permissions
"sandbox": true, "permissions": [] },
{ "url": "/app/plugins/third/community-thing.js", // sandboxed, community
"sandbox": true, "permissions": ["irc", "storage"] }
]
```
@ -56,7 +59,7 @@ The app's theme CSS vars (`--bg`, `--ink`, `--accent`, `--muted`, `--border`,
change, so plugin UI can use `var(--accent)` and look native in light + dark.
Examples: `src/modules/sandbox/features/dice.js` (a built-in, bundled + mounted by core) and
`public/plugins/orbit-sandbox-demo.js` (the minimal config-loaded form).
`public/plugins/third/orbit-sandbox-demo.js` (the minimal config-loaded form).
## Required CSP (APPLIED 2026-07-04)

View file

@ -50,10 +50,10 @@
"saslScram": true
},
"plugins": [
"/app/plugins/orbit-clock.js",
"/app/plugins/orbit-copy.js",
"/app/plugins/orbit-games.js",
"/app/plugins/orbit-invite.js"
{ "url": "/app/plugins/third/orbit-clock.js", "sandbox": true, "permissions": [] },
"/app/plugins/third/orbit-copy.js",
"/app/plugins/third/orbit-games.js",
"/app/plugins/third/orbit-invite.js"
],
"builtins": []
}

View file

@ -1,31 +0,0 @@
/*
* Orbit clock plugin adds a live HH:MM clock to the topbar.
*
* Demonstrates the `topbar_item` UI slot and using React hooks through
* orbit.React. Note the pattern: addUi returns an ELEMENT (orbit.h(Clock)), so
* Clock is a real component that owns its own hook state across re-renders.
*
* Load via config.json: "plugins": ["/app/plugins/orbit-clock.js"]
*/
Orbit.plugin('orbit-clock', (orbit, log) => {
const { useState, useEffect } = orbit.React;
function Clock() {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 15000);
return () => clearInterval(id);
}, []);
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
return orbit.html`
<span
title=${orbit.i18n.t('plugins.clock.title')}
style=${{ fontWeight: 600, fontSize: '.82rem', lineHeight: '1', opacity: 0.7, padding: '0 .45rem', alignSelf: 'center' }}
>${hh}:${mm}</span>
`;
}
orbit.addUi('topbar_item', () => orbit.h(Clock));
log('clock ready');
});

View file

@ -0,0 +1,24 @@
/*
* Orbit clock a live HH:MM clock in the topbar. FULLY SANDBOXED.
*
* Runs in an opaque-origin iframe reachable only through the capability bridge, so
* it can touch NOTHING outside itself: no page, no store, no IRC, no storage. It
* renders its own DOM (no React) and styles itself with the app's theme vars, which
* are mirrored into the sandbox so it still matches light + dark. Needs zero
* permissions.
*
* { "url": "/app/plugins/third/orbit-clock.js", "sandbox": true, "permissions": [] }
*/
Orbit.plugin('orbit-clock', function (orbit, log) {
orbit.ui('topbar_item', function (root) {
root.style.cssText = 'font:600 13px/1 system-ui,sans-serif;color:var(--ink,inherit);opacity:.7;white-space:nowrap';
root.title = 'Local time';
function tick() {
var d = new Date();
root.textContent = String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
tick();
setInterval(tick, 15000);
});
log('clock ready');
});