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

@tanqory/plugin-sdk

v0.6.3

Published

Build a Tanqory marketplace app. Server-side token exchange and REST client, webhook signature verification, a manifest schema, and a React hook that boots an embedded app inside the Tanqory dashboard.

Readme

@tanqory/plugin-sdk

Build an app for the Tanqory marketplace. You host it; Tanqory frames it, routes its API calls, and tells it who the merchant is.

npm install @tanqory/plugin-sdk

The two halves

import { createTanqoryApp, verifyWebhookSignature } from '@tanqory/plugin-sdk' // your server
import { useTanqoryApp } from '@tanqory/plugin-sdk/client' // your UI

Separate entry points on purpose: importing the server half must not drag React into a serverless function.

Embedded UI

'use client'
import { useTanqoryApp } from '@tanqory/plugin-sdk/client'

export default function Page() {
  const { ready, store, api } = useTanqoryApp()
  if (!ready) return <p>Loading…</p>

  return (
    <button onClick={() => api!.get('/orders', { limit: 5 }).then(console.log)}>
      Recent orders for {store!.storeName}
    </button>
  )
}

useTanqoryApp performs the whole handshake: verifying who framed you, requesting store context, retrying an unanswered handshake, holding a token in memory and renewing it before it expires, and reporting a dead session to the host. Getting any of that wrong produces a blank frame with a clean console, which is unpleasant to debug from the outside.

Server side

const tanqory = createTanqoryApp({
  clientId: process.env.TANQORY_CLIENT_ID!,
  clientSecret: process.env.TANQORY_CLIENT_SECRET!,
})

const orders = await tanqory.store(storeId).get('/orders', { query: { limit: 5 } })

Tokens are cached per store and renewals are single-flighted, so parallel work triggers one exchange rather than one each.

Webhooks

export async function POST(request: Request) {
  const raw = await request.text()
  if (!verifyWebhookSignature(raw, request.headers.get('x-tanqory-signature'), SECRET)) {
    return new Response('bad signature', { status: 401 })
  }
  …
}

Pass the raw body. Re-serialising a parsed object changes key order and whitespace, and the signature will not match.

Three things that will save you a day

Never name an API host. Every request is relative and Tanqory's edge resolves which regional cell holds the store from the id in the path. An app that hardcodes a hostname is pinned to one cell and breaks for merchants elsewhere.

Never send X-Frame-Options. It has no allow-list form, so SAMEORIGIN overrides your Content-Security-Policy: frame-ancestors in browsers that still read it, and the dashboard renders a blank frame. Set frame-ancestors instead.

Never navigate to a login page. You are inside an iframe: a login screen renders in a box, and if it escaped it would throw the merchant out of their dashboard. On auth failure the SDK tells the host and the host decides.

The manifest

tanqory.app.json is what review approves, and the only thing a version pins. Your code is yours — deploy whenever you like. Changing the manifest (a new scope, origin, or webhook topic) needs approval, because it changes what merchants agreed to.

{
  "handle": "order-notifier",
  "name": "Order Notifier",
  "appUrl": "https://order-notifier.example.com",
  "devUrl": "http://localhost:3000",
  "embedded": { "enabled": true, "path": "/embed" },
  "scopes": ["orders.view"],
  "webhooks": [{ "topic": "orders/create", "path": "/hooks/orders" }]
}

devUrl is used instead of appUrl, and only on a DEVELOPMENT store — so you can point at localhost while building without any localhost origin ever being trusted for a real merchant.

Validate it in your own build:

import { parseAppManifest } from '@tanqory/plugin-sdk'
parseAppManifest(JSON.parse(readFileSync('tanqory.app.json', 'utf8')))

It reports every problem at once rather than stopping at the first.