npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-bridge

Quick 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