@ethisyscore/app-bridge
v1.0.0-alpha.12
Published
EthisysCore Extension App Bridge — enables extension iframes to communicate with the EthisysCore host platform via MCP, navigation, toasts, theme sync, and user context.
Maintainers
Readme
EthisysCore App Bridge
@ethisyscore/app-bridge — Communication bridge between plugin UI iframes and the EthisysCore host platform.
Used by all plugin UIs regardless of backend language (.NET, Node.js, Python) or execution mode.
Installation
npm install @ethisyscore/app-bridgeQuick Start
import { createApp } from '@ethisyscore/app-bridge';
const app = await createApp({ version: '2026-02' });
// Invoke MCP tools through the host's authenticated session
const result = await app.mcp.invokeTool('my-tool', { key: 'value' });
// result: { content: '{"data": ...}', isError: false }
// Read MCP resources
const data = await app.mcp.getResource('resource://my-resource');
// Platform actions
app.navigate('/some/path');
app.showToast('Saved!', 'success');
const confirmed = await app.confirm({ title: 'Confirm', message: 'Are you sure?' });
// Open a dialog with plugin content
const dialogResult = await app.openDialog({
entrypoint: 'create-item.html',
title: 'Create Item',
width: 'lg',
});
if (dialogResult.action === 'submitted') {
const data = JSON.parse(dialogResult.data!);
}
// Open a side panel
app.openPanel({ entrypoint: 'detail.html', title: 'Details', width: 'md' });
// Listen for route changes (SPA mode)
app.onRouteChange((path) => {
router.navigate('/' + path);
});
// Guard unsaved changes
app.onBeforeUnload(() => form.isDirty ? false : true);
// Session storage (scoped per extension, survives navigation)
await app.setSessionData('draft', JSON.stringify(formState));
const draft = await app.getSessionData('draft');
// Cleanup when done
app.destroy();Context & Theme
// Get current user context
const ctx = await app.getContext();
// { organisationName, userName, userEmail, locale, permissions }
// Get current theme (full token payload)
const theme = await app.getTheme();
// { tokenVersion, mode, accessibility, tokens }
// Subscribe to changes (returns unsubscribe function)
const unsubTheme = app.onThemeChange((theme) => {
document.documentElement.setAttribute('data-theme', theme.mode);
});
const unsubCtx = app.onContextChange((ctx) => {
console.log('Context updated:', ctx.userName);
});
// Cleanup signal from host
app.onDestroy(() => {
// save state, close connections
});BridgeTheme
getTheme() and onThemeChange() return a BridgeTheme object with:
| Property | Type | Description |
|----------|------|-------------|
| tokenVersion | string | Semantic version of the token schema (e.g. "1.0.0") |
| mode | "light" \| "dark" | Current color mode |
| accessibility | BridgeThemeAccessibility | { highContrast, reducedMotion } |
| tokens | BridgeThemeTokens | Full design token tree (see below) |
onThemeChange fires with the complete BridgeTheme payload whenever the user toggles dark/light mode or accessibility settings change.
Design Token Categories
The tokens object contains 14 categories split into two tiers:
Core tokens (stable, unlikely to change between token versions):
| Category | Keys |
|----------|------|
| brand | primary, primaryLight, primaryDark, onPrimary |
| background | default, paper, subtle |
| text | primary, secondary, disabled, link |
| status | success, warning, error, info |
| border | subtle, strong |
| radius | sm, md, lg |
| spacing | unit |
| typography | fontFamily, fontSizeBase, lineHeightBase, fontWeightNormal, fontWeightMedium, fontWeightBold |
Extended tokens (additional detail for richer UI):
| Category | Keys |
|----------|------|
| focus | ring |
| interaction | hover, active, selected |
| overlay | scrim |
| divider | default |
| shadow | sm, md |
| input | borderDefault, borderHover, borderFocus, borderError, bg |
Navigation
Plugins can contribute sidebar navigation and dynamically control item visibility and badges at runtime.
Declaring Navigation
Navigation is declared in feature.manifest.json using NavigationContribution and NavigationItem:
{
"ui": {
"navigation": {
"id": "my-plugin",
"label": "My Plugin",
"icon": "Dashboard",
"location": "primary-nav",
"order": 50,
"children": [
{
"id": "dashboard",
"label": "Dashboard",
"path": "dashboard",
"requiredPermission": 16
},
{
"id": "settings",
"label": "Settings",
"path": "settings",
"requiredPermission": 8,
"initialVisibility": false
}
]
}
}
}Runtime Visibility & Badges
// Show a nav item that was initially hidden
await app.setNavVisibility('settings', true);
// Batch update multiple items at once
await app.setNavVisibilities({ 'settings': true, 'advanced': false });
// Set a badge on a nav item (ephemeral — clears on page refresh)
await app.setNavBadge('inbox', { text: '3', variant: 'error' });
// Clear a badge
await app.setNavBadge('inbox', null);ToolPermission
Permission bitmask used for navigation items, MCP tools, and RBAC. Combine with bitwise OR for composite masks.
import { ToolPermission } from '@ethisyscore/app-bridge';| Value | Name | Decimal |
|-------|------|---------|
| ToolPermission.None | No permission required | 0 |
| ToolPermission.Assign | Assign resources or roles | 1 |
| ToolPermission.View | View summary/listing data | 2 |
| ToolPermission.Delete | Delete records | 4 |
| ToolPermission.Write | Create or modify records | 8 |
| ToolPermission.Read | Read detailed record data | 16 |
| ToolPermission.Approve | Approve workflows | 32 |
| ToolPermission.PublishGlobal | Publish globally | 64 |
| Composites | | |
| ToolPermission.ReaderAccess | View + Read | 18 |
| ToolPermission.ContributorAccess | View + Write + Read | 26 |
| ToolPermission.OwnerAccess | All permissions | 127 |
API Reference
createApp(options)
Establishes the bridge connection. Returns a Promise<EthisysCoreApp>.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| version | string | (required) | API version (e.g., '2026-02') |
| timeoutMs | number | 10000 | Handshake timeout in milliseconds |
| mockResponses | Record<string, string \| BridgeToolResult> | — | Mock tool responses for standalone dev |
EthisysCoreApp
MCP
| Method | Description |
|--------|-------------|
| mcp.invokeTool(name, args) | Invoke an MCP tool, returns { content, isError } |
| mcp.getResource(uri) | Read an MCP resource, returns content string |
Navigation
| Method | Description |
|--------|-------------|
| navigate(path) | Navigate the host to a path (allowlisted routes only) |
| getRoute() | Get current sub-path within extension (e.g., "stats" when on /extensions/books/stats) |
UI: Toasts & Confirmation
| Method | Description |
|--------|-------------|
| showToast(message, type) | Show toast notification ('success', 'error', 'info') |
| confirm(options) | Show text confirmation dialog, returns boolean |
UI: Rich Dialog
| Method | Description |
|--------|-------------|
| openDialog(options) | Open modal dialog with plugin iframe content, returns DialogResult |
| closeDialog(result?) | Close current dialog, optionally pass data back to parent |
UI: Side Panel
| Method | Description |
|--------|-------------|
| openPanel(options) | Open overlay panel with plugin iframe content |
| closePanel() | Close current panel |
| updatePanel(options) | Update panel title/width without closing |
Navigation State
| Method | Description |
|--------|-------------|
| setNavVisibility(itemId, visible) | Show or hide a single navigation item |
| setNavVisibilities(overrides) | Batch-set visibility for multiple items (max 50) |
| setNavBadge(itemId, badge) | Set or clear badge on nav item (ephemeral) |
Context
| Method | Description |
|--------|-------------|
| getContext() | Get current user/org context |
| getTheme() | Get current theme tokens |
Utilities
| Method | Description |
|--------|-------------|
| copyToClipboard(text) | Copy text to clipboard (may show confirmation toast) |
| downloadFile(options) | Trigger file download from base64 content (max 10MB) |
Session State
| Method | Description |
|--------|-------------|
| setSessionData(key, value) | Persist key-value in host session storage (50 keys, 64KB/val, 1MB total) |
| getSessionData(key) | Read value from host session storage |
| clearSessionData() | Clear all session data for this extension |
Lifecycle Subscriptions
| Method | Description |
|--------|-------------|
| onRouteChange(cb) | Host route changed within extension (SPA mode). Update internal router. |
| onVisibilityChange(cb) | Tab became visible/hidden (Page Visibility API). Pause/resume polling. |
| onBeforeUnload(cb) | User navigating away. Return false to block (2s timeout). |
| onThemeChange(cb) | Theme or dark mode changed. Apply new tokens. |
| onContextChange(cb) | User/org context changed. Update UI. |
| onDestroy(cb) | Iframe about to unmount. Cleanup resources. |
| destroy() | Explicitly close the bridge connection |
UI Surfaces
DialogOptions
interface DialogOptions {
entrypoint: string; // Plugin page to render
title: string;
width?: "sm" | "md" | "lg" | "xl" | "full"; // default: "md"
height?: "sm" | "md" | "lg" | "full"; // default: "md"
closeOnOutsideClick?: boolean; // default: true
}DialogResult
interface DialogResult {
action: "closed" | "submitted";
data?: string; // JSON string from plugin
}PanelOptions
interface PanelOptions {
entrypoint: string; // Plugin page to render
title: string;
width?: "sm" | "md" | "lg"; // default: "md"
}NavBadge
interface NavBadge {
text?: string; // e.g., "3", "New"
variant: "info" | "warning" | "error" | "success";
}DownloadOptions
interface DownloadOptions {
content: string; // base64-encoded (max 10MB)
fileName: string;
mimeType: string;
}Standalone Development
When running outside the platform (no parent iframe), the bridge auto-detects standalone mode and provides mock responses. All calls are logged to the console.
Use mockResponses for custom mock data:
const app = await createApp({
version: '2026-02',
mockResponses: {
'my-tool': JSON.stringify({ items: [{ id: '1', name: 'Test' }] }),
},
});How It Works
Uses Penpal for bidirectional RPC over window.postMessage. The host exposes methods the plugin can call; the plugin exposes callback methods the host can invoke for event delivery (route changes, theme updates, visibility changes, destroy signal).
Build
npm ci
npm run build # tsup → ESM + CJS + declarations