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

@waler/sdk

v0.5.0

Published

The surface a Waler app talks to the shell through.

Readme

@waler/sdk

The SDK a Waler app embeds. A Waler app is a web page that a Waler shell loads in an iframe on the web and in a native WebView on iOS and Android; this package is how that page talks to the shell — session, context, tokens, navigation, device capabilities and events, over one bridge that behaves the same wherever the shell runs.

TypeScript, ESM, no runtime dependencies. Works with React, Vue, Svelte or plain HTML — the SDK has no opinion about your framework.

Install

npm install @waler/sdk

Quickstart

import { createSdk } from '@waler/sdk'

const Waler = createSdk({
  // Your app's registered id.
  appId: 'my-app',
  // The shell origins allowed to host this surface. A security boundary, not
  // configuration — see "Origins" below.
  allowedOrigins: ['https://tenant-one.example.com'],
})

// Resolves once the shell has answered. From here on, `context` is filled in
// and `capabilities` is populated.
const { api, context, theme, locale } = await Waler.ready

console.log(context.tenantId, context.userId, context.activeAudience)

ready rejects rather than resolving half-way. If it resolves, you have a live bridge and a real context; you never have to null-check what it returned.

Calling the platform

The browser never calls the platform API. getToken() returns the session of the person using your app, valid for a few minutes; send it to your own backend, which calls the platform with your app token and that session.

const { token } = await Waler.getToken()

const response = await fetch('/api/some-resource', {
  headers: { Authorization: `Bearer ${token}` },
})

Your backend validates the session against the platform's JWKS before trusting it. Keep the platform address and issuer in your backend's own configuration: api in the handshake is informational, and a value taken from the browser would let a forged one decide where your app token goes.

Sessions are held for you

Calling getToken() before every request is the right pattern: the result is kept until close to its expiry, so asking twice does not issue two sessions. Calls made at the same time share one issuance rather than one each.

It is memory only — nothing is written to localStorage or sessionStorage, and nothing survives a reload.

// Skips what is held and asks for a newly issued one.
const fresh = await Waler.getToken({ forceRefresh: true })

What is held is dropped whenever contextchange or audiencechange arrives — a session issued under the previous context is still accepted, carrying the previous answer, which is a failure with nothing to see. So a getToken() from inside those handlers gets a new session, not the one you had.

Confirming a write

Some writes need the person's explicit approval. When the platform answers one with a confirmation id, hand it to the shell: it shows the person what will happen, lets them edit what may be edited, and resolves with a single-use proof and the arguments as approved. Repeat the call with both.

const { token, args } = await Waler.requestConfirmation(confirmationId)

It rejects with CANCELLED if the person declines.

Feature detection, never version comparison

Your app is inside several shells at once, each on a different version, while the app itself is always on its latest deploy. So ask what the shell can do, never what version it is.

if (Waler.capabilities.has('scanBarcode')) {
  const { value } = await Waler.scanBarcode()
  fillField(value)
} else {
  showManualEntryField()
}

ready returns a shellVersion. It is for telemetry. Gating a feature on it is a bug that only shows up in the tenants that have not updated.

Capability-gated methods — notify, download, share, scanBarcode, getGeolocation — reject with UNSUPPORTED if you call them without checking.

Origins

allowedOrigins is the trust boundary, and the only thing standing between your surface and a page that embedded it without permission.

  • These are the only origins the SDK will send to, each addressed explicitly.
  • Messages from any other origin are dropped silently, before anything reads them.
  • There is no wildcard. '*' and 'https://*.example.com' are both refused — the first is not an origin, the second matches nothing and would leave you with an allowlist you believe covers every tenant and that in fact covers none.
  • An allowlist with nothing usable in it fails immediately with INVALID_ARGUMENT, rather than hanging until the handshake times out.

List the shell origins of the tenants your app is installed in:

createSdk({
  appId: 'my-app',
  allowedOrigins: ['https://tenant-one.example.com', 'https://tenant-two.example.com'],
})

On iOS and Android the shell opens your app in a native WebView of its own, and allowedOrigins is not used there. The native shell checks your app's origin against surfaces[].allowedOrigins in your manifest before it puts the bridge in the page, and only the main frame gets it.

Events

const off = Waler.on('contextchange', (context) => {
  // The shell resolved a different context, without reloading the surface.
  render(context)
})

// Call `off()` when your view goes away.

| Event | When | | ------------------ | ---------------------------------------------------------------- | | contextchange | the shell resolved a different context | | audiencechange | the user switched to another population | | commandupdate | a write this app started has a result | | themechange | the tenant's theme changed, or the system toggled dark mode | | localechange | language or timezone changed | | visibilitychange | the surface left or came back — pause polling when invisible |

Two things to assume about commandupdate, because writes are asynchronous: it may never arrive (rebuild state when your view opens), and it may arrive twice (handle it idempotently, keyed on commandId). Without both, a "processing…" spinner can run forever with no error anywhere.

Errors

Everything rejects with an SdkError carrying a code:

import type { SdkError } from '@waler/sdk'

try {
  await Waler.scanBarcode()
} catch (error) {
  const { code } = error as SdkError
  if (code === 'UNSUPPORTED') showManualEntryField()
  else if (code === 'CANCELLED') return
  else throw error
}

| Code | Meaning | | ------------------ | -------------------------------------------------------------- | | UNSUPPORTED | this shell does not offer the method or capability — fall back | | FORBIDDEN | not granted in the manifest, or denied | | CANCELLED | the user backed out | | TIMEOUT | the shell did not answer in time | | INVALID_ARGUMENT | bad input, including an unusable allowedOrigins | | NOT_IN_SHELL | running outside the shell | | INTERNAL | everything else |

Treat the list as open: a newer shell may send a code this version has never heard of, and it reaches you unchanged rather than flattened. Always keep an else.

The stack on these errors points at your await, not at the SDK's internals.

Running without a shell

Open your app directly in a browser tab and the SDK falls back to a null transport: everything rejects with NOT_IN_SHELL. That is a designed path, not a failure — it is how you develop and test without standing up a shell.

import type { SdkError } from '@waler/sdk'

try {
  const { context } = await Waler.ready
  render(context)
} catch (error) {
  if ((error as SdkError).code === 'NOT_IN_SHELL') render(mockContext)
  else throw error
}

Surface

| Member | | | -------------------------- | ----------------------------------------------------------- | | ready | Promise<ReadyPayload> — resolves only over a live bridge | | context | current ShellContext, or null before ready resolves | | capabilities | .has(name) and .list() | | getToken(request?) | the person's session, for your own backend | | requestConfirmation(id) | asks the person to approve a write, resolves with the proof | | openApp(key, params?) | opens another surface, resolves with what it returned | | navigate(path) | tells the shell your current route, for deep links and back | | close(result?) | closes this surface, resolving whoever opened it | | setHeader(state) | title, badge, or hide the native header | | on(event, handler) | returns an unsubscribe function | | invoke(method, payload?) | escape hatch for methods newer than this SDK |

navigate() and setHeader() are explicit on purpose: the SDK does not patch history.pushState or watch <title> to infer them, because it will not rewrite globals it does not own inside your app.

Everything is typed. ReadyPayload, ShellContext, SdkError, Capability, SdkEventMap and the rest are exported from the package root.

Versioning and compatibility

This package follows semver. A breaking change is a major, and what it replaces is deprecated first: the old member keeps working and the docs say what to use instead, until the major removes it. Pin what you install and upgrade when it suits you.

That covers the package. It does not cover the shell, and the difference is the one thing worth understanding here.

Your app does not choose which shell loads it. The shell is an installed application that may be older than your app, and several ages of it are in use at the same time. So a version comparison cannot help you — the shell is not a dependency you picked, it is the thing you are running inside of.

Ask what it can do, never how old it is:

if (Waler.capabilities.has('scanBarcode')) await Waler.scanBarcode()
else showManualEntryField()

The bridge is built so that this always has an answer. A method the shell does not know rejects with UNSUPPORTED instead of hanging, and an error code this package does not know reaches your catch unchanged. Neither is a promise about a period of time; both are how the bridge behaves, in every version.

Requirements

Any browser the shell supports. Node 20+ if you import it in a build step. Published as ESM only, with no runtime dependencies.