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

@tether-app/module-sdk

v0.1.1

Published

Official SDK for building Tether third-party modules. Provides the browser-side postMessage bridge client and the Node-side webhook signature verifier.

Readme

@tether-app/module-sdk

Official SDK for building Tether third-party modules. Provides the two most error-prone pieces of the platform contract as thin, well-tested helpers so module authors don't reimplement them (and subtly get them wrong) from scratch.

  • Browser: connectToTether() — wraps the iframe postMessage bridge. Handshake, state hydration, event subscriptions, outbound actions, lifecycle cleanup.
  • Node: verifyTetherWebhook() — wraps HMAC-SHA256 signature verification and the 5-minute replay window check required by /platform/v1 webhook deliveries. Uses a constant-time comparison internally so callers can't introduce timing oracles.

This SDK is the canonical client for the wire contract documented in docs/platform-api.md. If anything in this README disagrees with that document, the platform-api doc is authoritative — file a bug.


Installation

npm install @tether-app/module-sdk

The package ships two conditional exports so you only pull in the side you need:

import { connectToTether }    from '@tether-app/module-sdk/browser'
import { verifyTetherWebhook } from '@tether-app/module-sdk/node'

Node 18+ is required (for global crypto and fetch). Types are included — hand-written, not generated, so the surface is precise.


Browser: connectToTether

Call from inside the iframe Tether hosts for your module. The SDK handles the tether:ready handshake, waits for the initial tether:session-state response, and returns a handle with live state getters and event subscriptions.

import { connectToTether } from '@tether-app/module-sdk/browser'

const tether = await connectToTether()

console.log('session:', tether.sessionId, 'lobby:', tether.lobbyId)
console.log('host:', tether.isSessionHost)
console.log('initial participants:', tether.participants)

// Subscribe to updates. Every on*() returns an unsubscribe fn.
const offState = tether.onStateChange((state) => {
  render(state)
})

tether.onParticipantsChange((participants) => {
  updatePresenceUi(participants)
})

tether.onModuleEvent((eventName, payload) => {
  // Events broadcast by OTHER session viewers via Tether's relay
  // or via your backend's POST /platform/v1/sessions/:id/broadcast
  if (eventName === 'dice:rolled') showRoll(payload)
})

tether.onSessionEnded(() => {
  // The host ended the session. Tear down local state.
  teardown()
})

// Outbound actions
tether.emit('dice:rolled', { d20: 17 })   // tether:emit-event
tether.requestParticipants()              // tether:request-participants
tether.leave()                            // tether:leave — navigates user back to the lobby

Options

connectToTether({
  timeoutMs:    10_000,         // reject if no session-state arrives
  targetWindow: window,         // override for tests (no-ops in prod)
  parentWindow: window.parent,  // override for tests (no-ops in prod)
})

Rejection reasons

The returned promise rejects if:

  • The current window is not inside an iframe (parentWindow === targetWindow or window.parent unavailable)
  • No initial tether:session-state response arrives within timeoutMs (default 10s)

Security model

  • The SDK validates that event.source of every inbound message matches the parent window — a sibling iframe cannot impersonate the host.
  • It intentionally does NOT validate event.origin. Tether's host iframe is sandboxed without allow-same-origin, so the iframe runs in an opaque origin and cannot reliably know the parent's real origin. The event.source identity check is the strong guarantee here. See the header comment in browser/index.js for full rationale.
  • Unknown inbound message types are silently dropped.
  • Unknown keys on known messages are passed through (forward-compat for future protocol additions like A/V callState).

Node: verifyTetherWebhook

Call from your webhook receiver to verify signed deliveries from Tether's outbound webhook dispatcher. The function is synchronous — all inputs are already in memory. It never throws for protocol-level failures; bad signatures, stale timestamps, and missing headers are reflected in the valid: false result with a stable reason code.

Plain Node http server

import http from 'node:http'
import { verifyTetherWebhook, readRawBody } from '@tether-app/module-sdk/node'

const SECRET = process.env.TETHER_WEBHOOK_SECRET

http.createServer(async (req, res) => {
  const rawBody = await readRawBody(req)

  const result = verifyTetherWebhook({
    headers: req.headers,
    rawBody,
    secret: SECRET,
  })

  if (!result.valid) {
    // Return 401 for any unverifiable request. Do NOT leak the
    // reason — Tether treats 4xx as "don't retry", which is what
    // you want for any bad-signature case.
    res.writeHead(401); res.end(); return
  }

  console.log('event:', result.event, 'payload:', result.payload)
  // result.moduleId and result.timestamp are also available.

  res.writeHead(200, { 'content-type': 'application/json' })
  res.end('{"ok":true}')
}).listen(4100)

Express with body-parser

Express strips the raw body by default. Configure express.raw() for the webhook route so the SDK can verify the signed bytes exactly as received:

import express from 'express'
import { verifyTetherWebhook } from '@tether-app/module-sdk/node'

const app = express()
const SECRET = process.env.TETHER_WEBHOOK_SECRET

app.post(
  '/tether/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const result = verifyTetherWebhook({
      headers: req.headers,
      rawBody: req.body,  // Buffer, per express.raw()
      secret:  SECRET,
    })
    if (!result.valid) return res.sendStatus(401)

    // req.body is still the raw Buffer here. The parsed event and
    // payload come from result, which verified them against the
    // signature before parsing.
    handleEvent(result.event, result.payload)
    res.json({ ok: true })
  }
)

Do not re-serialize the parsed body and pass it back in as rawBody. JSON whitespace and key order are part of the signed bytes; re-stringifying almost always invalidates the signature.

AWS Lambda / API Gateway

import { verifyTetherWebhook } from '@tether-app/module-sdk/node'

export const handler = async (event) => {
  const result = verifyTetherWebhook({
    headers: event.headers,
    rawBody: event.isBase64Encoded
      ? Buffer.from(event.body, 'base64').toString('utf8')
      : event.body,
    secret: process.env.TETHER_WEBHOOK_SECRET,
  })
  if (!result.valid) return { statusCode: 401, body: '' }

  await handleEvent(result.event, result.payload)
  return { statusCode: 200, body: '{"ok":true}' }
}

Result shape

Success:

{
  valid: true,
  event: 'session:created',
  payload: { moduleSessionId: '...', lobbyId: '...', participants: [...] },
  moduleId: 'com.example.module',
  timestamp: 1700000000,
}

Failure:

{ valid: false, reason: 'signature_mismatch' }

Failure reasons (stable machine codes):

| reason | meaning | | ------------------------- | ------- | | missing_secret | secret arg was empty or not a string | | missing_signature | X-Tether-Signature header absent | | missing_timestamp | X-Tether-Timestamp header absent | | invalid_timestamp | Header value is not a pure decimal integer | | replay_window_exceeded | Timestamp is more than 300 seconds from receiver's clock | | signature_mismatch | HMAC-SHA256 digest did not match (body tampered, wrong secret, or malformed sig) | | invalid_body | rawBody was not a string or Buffer, was not valid JSON, was not an object, or lacked an event name |

Idempotency

Tether's delivery pipeline retries on 5xx and network errors up to WEBHOOK_MAX_ATTEMPTS (default 3). Your receiver MUST be idempotent — a 2xx response followed by a lost network ack will result in the same event being redelivered. Dedupe on (moduleId, moduleSessionId, event, timestamp) or an equivalent tuple if strict once-delivery semantics matter for your module.


Running the SDK's own tests

npm run test:sdk

Runs the fourth vitest surface (node env, no jsdom) from the repo root. Currently covers both helpers with hand-rolled fakes rather than real DOM / real HTTP — the suite is fast (single-digit ms per case) and independent of the browser / jsdom toolchain.

See packages/module-sdk/test/ for the vectors. The wire-format lock test in test/node.test.js is the single most important case: if it ever fails, either the SDK or the backend's signBody() drifted and any in-flight third-party receiver will start 401'ing on the next deployment.


Versioning and stability

v0.1 — the API has been dogfooded end-to-end against the Tether testbed module (both the iframe bridge and the webhook receiver) and survived without requiring changes. The surface is stable for third-party consumption.

Past v1.0 the API surface will be frozen and changes will follow semver:

  • Bug fixes → patch bump.
  • New optional fields on the result / handle → minor bump.
  • Any change to required fields, failure reasons, or wire strings → major bump, with the Tether platform continuing to support the previous major for one deprecation cycle.