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

@green-ecolution/plugin-interface

v1.1.0

Published

SDK for building a Green Ecolution plugin: the browser-side handshake between the host application and a plugin's iframe, plus a small React helper around it.

Downloads

226

Readme

@green-ecolution/plugin-interface

SDK for building a Green Ecolution plugin: the browser-side handshake between the host application and a plugin's iframe, plus a small React helper around it.

A Green Ecolution plugin is an external system, installed by an administrator through the application's settings, that may do either or both of two things: write data into Green Ecolution through its ingest API, and contribute its own view, embedded in an iframe inside the application. This package covers the second half, the view. The first half, writing data, is a plain HTTP contract described below and needs no SDK at all.

There is no Module Federation here, no remote bundle loading, and no shared React runtime between host and plugin. A plugin is an ordinary web application that happens to run inside an iframe and speaks one small postMessage protocol to its host.

A complete working plugin using this SDK lives in frontend/demo-plugin: it renders the context from the handshake and imports demo trees through the ingest contract described below.

Installation

pnpm add @green-ecolution/plugin-interface

React 19 is a peer dependency; install react and react-dom alongside it if your plugin does not already depend on them.

How a plugin is registered

A plugin never registers itself. An administrator creates it from Settings → Plugins in Green Ecolution, choosing a slug, the organization it belongs to, the permissions it receives, and, if it has a view, where that view lives (an external https URL, or an in-cluster service address the operator proxies). Installing issues a plaintext API key exactly once; that key is what the plugin's own backend adapter uses to call the ingest endpoints below. There is no self-service registration call to make from plugin code.

If your plugin only pushes data and has no view, you can stop reading here and go straight to the ingest contract.

The view: embedding and the handshake

When a plugin has a view, Green Ecolution renders it in a sandboxed iframe (allow-scripts allow-forms allow-popups allow-same-origin, referrerPolicy="no-referrer", allow only lists the device capabilities an administrator granted the plugin (camera, bluetooth), and is empty otherwise) and waits for it to say hello. The protocol is a small envelope carried over window.postMessage:

type Envelope<T> = {
  ns: 'green-ecolution'
  v: 1
  type: string
  payload: T
}

Three message types make up the current protocol:

  • ge:hello — sent by the plugin to its parent window on load, empty payload.
  • ge:init — sent by the host in reply, carrying the PluginContext.
  • ge:resize — sent by the plugin whenever its content height changes, { height: number }.

Both sides check more than the message shape before trusting it: the host only replies to a ge:hello whose event.source is the iframe's own contentWindow (checking origin alone would accept a forged message from any other same-origin frame on the host page), and the plugin only accepts a ge:init whose event.source is window.parent. None of this carries a secret, so the target origin on the plugin's side is '*'; the check is about which window sent the message, not about hiding its contents. The view ticket is the one exception, which is why the host answers only its own iframe's contentWindow on the plugin's registered origin.

connectToHost()

Call this once, as early as possible, from inside your plugin's own document. It sends ge:hello and resolves with the PluginContext once the host answers:

import { connectToHost } from '@green-ecolution/plugin-interface'

const context = await connectToHost()

The hello is repeated every 250 ms until the host answers, because the host attaches its listener from an effect and a single hello can arrive before it is listening. After 10 seconds without an answer the promise rejects rather than hanging, which is what you see when the document is opened directly instead of embedded as a plugin view. Both values are adjustable, and 0 disables either behaviour:

const context = await connectToHost({ retryIntervalMs: 100, timeoutMs: 0 })

notifyResize(height)

Call this whenever your content's height changes, so a host that acts on it can size the iframe accordingly:

import { notifyResize } from '@green-ecolution/plugin-interface'

notifyResize(document.documentElement.scrollHeight)

PluginProvider and usePluginContext

For a React plugin, PluginProvider wraps connectToHost() in a component that renders its children only once the handshake has completed, so consumers never have to handle a not-yet-connected state themselves:

import { PluginProvider, usePluginContext } from '@green-ecolution/plugin-interface'

function App() {
  return (
    <PluginProvider>
      <TreeImportStatus />
    </PluginProvider>
  )
}

function TreeImportStatus() {
  const { locale, theme, user, plugin } = usePluginContext()
  return (
    <p>
      {user.displayName} · {plugin.slug} · {locale} · {theme}
    </p>
  )
}

Two optional props cover the states before and instead of a completed handshake. pending is rendered while it runs (nothing by default), and fallback receives the error when the host never answered. Without a fallback the provider renders the error message itself, so a plugin opened outside the host says so rather than showing a blank page:

<PluginProvider pending={<Spinner />} fallback={(error) => <ErrorPage error={error} />}>
  <TreeImportStatus />
</PluginProvider>

PluginContext

interface PluginContext {
  locale: 'de' | 'en'
  theme: 'light' | 'dark'
  user: { displayName: string }
  plugin: { slug: string }
  /**
   * Single-use, valid for two minutes. Not a credential for the Green
   * Ecolution API: hand it to your own backend, which redeems it with the
   * plugin key to learn who opened the view.
   */
  viewTicket?: string
}

This is presentation context plus one optional ticket: a display name to greet the operator with, the interface language and colour scheme to match, the plugin's own slug, and viewTicket. The ticket is not a credential for the Green Ecolution API. It is single-use, valid for two minutes, and only your plugin can redeem it: send it to your own backend, which calls POST /api/v1/plugins/view-tickets/redeem with the plugin key and { "ticket": "<viewTicket>" }. The answer names the user who opened the view (user.id, user.display_name) and the plugin's organization_id. Use it to start your own session for the view; every new handshake carries a fresh ticket. An expired, already redeemed or foreign ticket answers 401 with code plugin.view_ticket_invalid; a disabled plugin gets 403.

The ingest contract

This part needs no SDK, just an HTTP client and the API key issued when the plugin was installed. Send it as a bearer token:

Authorization: Bearer gep_<uuid>.<secret>

All ingest endpoints live under /api/v1/plugins/ingest/ and one identity endpoint at /api/v1/plugins/me; none of them require a user session. A disabled plugin, or an instance with the plugins feature turned off, answers 403 / 503 respectively, with a JSON body { "error": "...", "code": "..." }.

Confirm identity

GET /api/v1/plugins/me
{
  "id": "01990000-0000-7000-8000-000000000001",
  "slug": "tbz-baumkataster",
  "name": "TBZ Baumkataster",
  "description": null,
  "organization_id": "01980000-0000-7000-8000-000000000001",
  "permissions": ["tree:create", "tree:update"],
  "required_permissions": ["tree:read"],
  "frontend_mode": "none",
  "frontend_target": null,
  "enabled": true,
  "has_credential": true,
  "last_seen_at": "2026-09-01T10:00:00Z",
  "created_at": "2026-08-01T00:00:00Z"
}

Use this before importing anything to confirm the plugin is enabled and to read its own organization_id and granted permissions rather than hard-coding them.

Upsert trees in a batch

POST /api/v1/plugins/ingest/trees

Up to 500 entries per request, matched by external_id, an opaque identifier your adapter invents and reuses on every later run for the same tree. Requires tree:create and tree:update among the plugin's own permissions.

{
  "items": [
    {
      "external_id": "12345",
      "number": "FL-001",
      "species": "Quercus robur",
      "planting_year": 1998,
      "latitude": 54.7836,
      "longitude": 9.4321,
      "description": null,
      "additional_info": { "objectid": 12345 }
    }
  ]
}

Every entry is processed independently, so one malformed entry never fails the rest of the batch; the response is 200 even when some entries failed:

{
  "results": [
    {
      "external_id": "12345",
      "status": "created",
      "tree_id": "01990000-0000-7000-8000-000000000001"
    }
  ],
  "summary": { "created": 1, "updated": 0, "unchanged": 0, "failed": 0 }
}

status is one of created, updated, unchanged or failed. A failed entry carries an error string instead of a tree_id. Running the same batch again once nothing has actually changed reports unchanged, so a full re-import on every run is safe and cheap to repeat. A request with more than 500 items is rejected outright with 413 and code plugin.batch_too_large, before any entry is processed.

Delete a tree by external id

DELETE /api/v1/plugins/ingest/trees/{external_id}

Deletes the tree this external_id resolves to and returns 204. Requires tree:delete. Answers 404 if this plugin has no reference under that external_id.

List your own tree references

GET /api/v1/plugins/ingest/trees?limit=100&cursor=12344

A keyset page over this plugin's own external_id → tree mappings, ordered by external_id, useful for reconciling what Green Ecolution still has on file against your own source system:

{
  "items": [{ "external_id": "12345", "tree_id": "01990000-0000-7000-8000-000000000001" }],
  "next_cursor": "12345"
}

Pass next_cursor back as cursor to fetch the next page; a response with no next_cursor is the last page. There is no total; walk the pages to the end rather than trying to estimate how many are left.

List sensor models

GET /api/v1/plugins/ingest/sensor-models

Lists the sensor models a plugin can reference when it creates a sensor. Requires sensor:read.

Create a sensor

POST /api/v1/plugins/ingest/sensors

Creates a sensor with the same body as the user-facing sensor creation, minus organization_id and provider: the sensor always belongs to the plugin's organization and its provider is the plugin's slug. Requires sensor:create; answers 201, 400, 403, 404 for an unknown model_id, or 409.

Proxied plugins

A plugin that runs inside the cluster can let Green Ecolution serve its view under <scheme>://<slug>.<plugin host suffix>/, where the scheme is the one the instance is configured with (https in production, plain http in a local setup). The administrator enters the service as host:port; the host must be a plain DNS name and, together with the port, on the instance's allowlist.

The path /__ge/session on the plugin host belongs to Green Ecolution for every method: it exchanges the view ticket for the session cookie and never reaches the plugin. The session cookie is SameSite=Lax, so the plugin host has to be same-site with the app (for example app.example.org and kataster.plugins.example.org). On a host from another site the browser drops the cookie inside the iframe and every request answers 401.

Every request reaches the plugin with two headers set by Green Ecolution:

  • X-GE-User-Id: the viewer's user id.
  • X-GE-User-Name: the viewer's display name, percent-encoded (RFC 3986).

Incoming X-GE-* headers from the browser are dropped, so the values are trustworthy as long as the pod accepts traffic from Green Ecolution only (enforce that with a NetworkPolicy). Authorization never reaches the plugin. The view runs at the root of its own host, so absolute paths like /api/... work, and its own cookies stay on that host. The viewTicket in the handshake context is not needed in this mode. The plugin handles its own CORS; the proxy adds a frame-ancestors CSP limited to the app origins unless the plugin sends its own.

What a plugin cannot do

A plugin acts only within the organization it was installed into and only with the permissions it was granted there, exactly like a user with a role scoped to that organization. It cannot see or modify data belonging to another organization, and it cannot exceed the permission set the installing administrator gave it. A tree it created can later be moved to another organization by an administrator; the plugin's own reference to it survives that move, but further updates or deletes then require the plugin to still hold the matching permission in the tree's new organization.

License

AGPL-3.0-only