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

@onebooks/app-bridge

v0.1.0

Published

Browser SDK for OneBooks embedded apps: App Bridge protocol v1 (postMessage) — session tokens, host navigation, toasts, dialogs and auto-resize.

Readme

@onebooks/app-bridge

Browser SDK for OneBooks embedded apps — the App Bridge protocol v1 (postMessage-based) that lets your app page talk to the OneBooks host: session tokens, navigation, toasts, dialogs, resizing and theme/locale events. See Contract 4 in the app-platform plan for the wire protocol this package implements.

Zero runtime dependencies. ESM, TypeScript, Node ≥ 20 to build/test (the built artifact runs in any modern browser).

Install

npm install @onebooks/app-bridge

You can also load the pre-bundled global build directly with a <script> tag — copy dist/app-bridge.global.js next to your page; see Script tag usage below.

Quickstart

Your app is loaded inside an <iframe> by the OneBooks host with a ?host= query parameter (plus locale, embedded, and for extensions target, resourceType, resourceId). createApp() reads all of that for you:

import { createApp } from '@onebooks/app-bridge';

const app = createApp(); // host/locale/etc. read from location.search

await app.ready(); // tell the host you're loaded

const ctx = await app.context();
console.log(ctx.business.name, ctx.locale, ctx.theme);

// Call your own backend with an auto-attached session token:
const res = await app.fetch('/api/invoices/recent');
const invoices = await res.json();

await app.toast('Loaded!', { tone: 'success' });

API reference

createApp(options?)

createApp(options?: { host?: string; timeoutMs?: number }): OneBooksApp
  • host — the OneBooks host origin to talk to. Defaults to the ?host= query parameter. Must be an https: origin, or http://localhost (any port) for local development — anything else throws AppBridgeError with code INVALID_HOST (or MISSING_HOST if neither is present).
  • timeoutMs — how long to wait for a host response before rejecting. Default 10000.

Throws synchronously (does not return a rejected promise) if host is missing or invalid, since without it nothing can be sent safely.

app.params

The parsed embed query parameters: { host, locale?, embedded, target?, resourceType?, resourceId? }.

app.ready()

Tell the host your app finished loading. Call this first, before anything else. Resolves { hostVersion: number }.

app.sessionToken()

Resolves a short-lived bearer token scoped to your app's backend. Cached until ~10s before its expiresAt, and concurrent calls share a single in-flight request (single-flight) — safe to call from multiple places without worrying about request storms. You rarely need this directly; use app.fetch() instead.

Because the token is reused for up to ~50 seconds, your backend must not exchange it on every request: a session token can be exchanged at POST /oauth/token only once (a replay is rejected with invalid_grant). Verify the session token on every request, exchange it only when you have no stored token pair for that (bid, sub) yet, and cache the access/refresh pair you get back — see @onebooks/node's verifySessionToken / oauth.exchangeSessionToken.

app.context()

Resolves { locale, dir, theme, business: { name, country, currency }, user: { name }, target: { type, resourceType?, resourceId? } | null }.

app.toast(message, options?)

Shows a host-rendered toast. options.tone is 'info' | 'success' | 'error'. message should stay under ~200 characters — the host truncates longer text (an empty message is rejected with INVALID_PAYLOAD). At most 5 toasts per 10 seconds per frame; more reject with RATE_LIMITED.

app.navigate.host({ resource, id? })

Navigates the host to one of its own screens: resource is one of 'invoice' | 'quote' | 'customer' | 'supplier' | 'purchase' | 'sales-return' | 'purchase-return' | 'item-list' | 'dashboard'.

app.navigate.app(path)

Asks the host to change your app's iframe location to path — a same-app path starting with a single / (at most 2048 characters, no //, \ or ://). A path with a . or .. segment — literal or percent-encoded (%2e, %2E%2e, …) — is rejected with INVALID_PAYLOAD rather than normalised. Only available on an app page; blocks and modals get NOT_PERMITTED.

app.close(result?)

Closes your app's modal (an action extension) and hands result back to the host. Only available in a modal; pages and blocks get NOT_PERMITTED.

app.resize(height) / app.autoResize()

resize(height) reports your content height in pixels (clamped 60–1600 by both the SDK and the host). Blocks and modals only — a full app page is sized by the host (NOT_PERMITTED).

autoResize() keeps the frame matched to your content for you — call it once after your content mounts:

const stopAutoResize = app.autoResize();
// later, if you tear the app down without a full page unload:
stopAutoResize();
  • It measures your content, not the frame. The height it reports is that of your page's <body> content — an auto-height <body>'s own height plus its top and bottom margins — and it observes <body> with a throttled ResizeObserver: it reports the initial size straight away, then calls resize() whenever the height changes (at most every 150 ms). So the frame shrinks as well as grows.
  • Leave <body> at its natural height. Don't give <body> a fixed or 100% height (or a min-height of 100vh): autoResize() would then measure that height instead of your content, and the frame would stop following it.
  • Its resize requests are fire-and-forget. A host that refuses one (on a full app page, which the host sizes itself) or doesn't answer never surfaces as an unhandled rejection. Call resize() yourself when you need to know the outcome.
  • If ResizeObserver isn't available in the embed context, autoResize() becomes a no-op (and its return value a no-op cleanup) rather than throwing. A document with no <body> is measured and observed through <html> instead.

app.confirm({ title, message, confirmLabel?, destructive? })

Host-rendered confirmation dialog. Resolves true when the merchant confirms and false when they cancel or dismiss it — cancelling never rejects. title ≤ 80 chars, message ≤ 500 chars, confirmLabel ≤ 30 chars. The dialog names your app as the one asking.

One dialog at a time per frame: while a confirm() or pick() dialog is open, a second call rejects with RATE_LIMITED (it never replaces the dialog the merchant is reading) — await the first before opening another.

app.pick({ resource, multiple? })

Host-rendered resource picker ('customer' | 'supplier' | 'item' | 'invoice'). Resolves an array of { id, label } — an empty array when the merchant cancels (the host's CANCELLED answer is turned into [], so no try/catch is needed for an ordinary cancel). Only works if your installation holds the resource's read scope; otherwise it rejects with NOT_PERMITTED. Like confirm(), only one dialog per frame at a time — a second call while one is open rejects with RATE_LIMITED.

const [customer] = await app.pick({ resource: 'customer' });
if (!customer) return; // cancelled

app.loading(loading)

loading(true) shows a thin progress bar across the top of your app's area; loading(false) removes it. It only appears once your app has called ready().

app.setTitle(title)

Adds a secondary heading (≤ 80 chars) next to your app's registered name in the host chrome — e.g. Parcel Labels · Shipment 1042. It never replaces the registered name: who the content is from is always shown as registered. Blocks have no chrome, so there it has no visible effect.

What merchants see around your app

The host always attributes your content, whatever your page renders: an app page or modal sits under a bar showing your registered app name (plus any setTitle() heading), your developer name, and "This content is provided by {app}, not OneBooks." A block shows your extension's label with "Provided by {app} ({developer}), not OneBooks" beneath it — the registered app and developer names, never the label you chose.

app.on(event, callback)

Subscribes to host-pushed events: 'theme.changed' ({ theme }) and 'locale.changed' ({ locale, dir }). Returns an unsubscribe function. Events are sent only after your app has called ready().

A OneBooks language change does not reload your frame. The embed URL's locale parameter (and context().locale) is only the starting language; when the merchant switches language, the host sends locale.changed and your page must re-render itself:

const off = app.on('locale.changed', ({ locale, dir }) => {
  document.documentElement.lang = locale;
  document.documentElement.dir = dir;
  render(locale);
});
app.on('theme.changed', ({ theme }) => applyTheme(theme));
// later
off();

app.fetch(input, init?)

A fetch() wrapper for calling your own backend. If the request target is same-origin with the embedded page, it automatically attaches Authorization: Bearer <session token> (fetching/caching the token as needed). If the target is a different origin, the request is passed through unmodified — the session token is never attached to, or leaked toward, a third-party origin.

// same-origin — gets the Authorization header
await app.fetch('/api/invoices/recent');

// cross-origin — sent as-is, no token attached
await app.fetch('https://cdn.example.com/logo.png');

Errors

Every rejected promise from this SDK carries an AppBridgeError:

interface AppBridgeError extends Error {
  code: string; // see below
  message: string;
}

| Code | Meaning | |---|---| | UNKNOWN_ACTION | Sent by the host — your SDK/host versions are out of sync. | | INVALID_PAYLOAD | The host rejected your request's payload shape. | | NOT_PERMITTED | Your installation lacks the scope/permission for this action. | | CANCELLED | The user dismissed a dialog/picker. confirm() and pick() never reject with it — they resolve false and [] instead; it only reaches you from actions with no natural "no" result. | | RATE_LIMITED | Too many toasts (more than 5 per 10 s), or a confirm()/pick() while another dialog from your frame is still open. | | INTERNAL | Unexpected host-side error. | | MISSING_HOST | Raised locally by createApp() — no host available. | | INVALID_HOST | Raised locally by createApp() — host isn't https or localhost. | | TIMEOUT | Raised locally — the host didn't respond within timeoutMs. |

Script tag usage

npm run build also produces dist/app-bridge.global.js, a self-contained IIFE bundle for apps that don't use a bundler:

<script src="/static/app-bridge.global.js"></script>
<script>
  const app = OneBooksAppBridge.createApp();
  app.ready();
</script>

The bundle exposes window.OneBooksAppBridge = { createApp, AppBridgeError }. The SDK's version (sent to the host in ready) is exported as SDK_VERSION from the ES module build.

(The developer-sdk/templates/embedded-node example app does exactly this — it copies dist/app-bridge.global.js into its own static/ folder at build time.)

Security model

  • Every response is accepted only if both event.origin === host and event.source === window.parent — origin alone is spoofable by anything else the page loads into another frame/window.
  • app.fetch() only attaches the session token to same-origin requests.
  • The session token itself is short-lived (~60s server-side) and refreshed transparently; this SDK never persists it (no localStorage, no cookies).

Development

npm install
npm run build      # tsc -> dist/, then esbuild -> dist/app-bridge.global.js
npm test           # node:test via tsx, using a fake window/parent (no browser needed)
npm run typecheck

License

MIT