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

@stacknet/keyutils

v0.9.8

Published

Reusable components for StackNet node keys and noncustodial node key pools

Readme

@stacknet/keyutils

Reusable React components, hooks, and clients for StackNet node keys and noncustodial node key pools.

Install

pnpm add @stacknet/keyutils

Peer dependencies: react >= 18, react-dom >= 18

Quick Start

import '@stacknet/keyutils/styles.css';
import { BuyKeyWidget } from '@stacknet/keyutils/components';

<BuyKeyWidget
  apiBaseUrl=""
  stackId="stk_your_stack_id"
  stackName="My App"
  paymentMethods={['stripe', 'solana']}
  merchantWallet="YOUR_SOLANA_WALLET_ADDRESS"
  maxQuantity={5}
  keyImage="/key-art.png"
  aispImage="/aisp-chip.png"
  onMintSuccess={(result) => console.log('Key generated:', result)}
/>

The BuyKeyWidget wraps its own KeyUtilsProvider and manages state internally. Import the package stylesheet once in your app entry point (for Next.js, app/layout.tsx).

Styles and theme (0.9.0)

The package ships compiled CSS and local DM Sans and JetBrains Mono fonts. Consumers do not need Tailwind, a content scan, a font service, or a CDN. Keep the stylesheet import when upgrading from an earlier release.

Dark mode is the default, independent of the host page. Set theme="light" or theme="system" on BuyKeyWidget or NodeKeyPoolsWidget to opt in. For individual panels, set config.theme on KeyUtilsProvider. The system option follows the device preference through CSS and is safe for server rendering.

import '@stacknet/keyutils/styles.css';
import { KeyUtilsProvider, NodeKeyPoolsPanel } from '@stacknet/keyutils/components';

<KeyUtilsProvider config={{ apiBaseUrl: '', theme: 'dark', pools }}>
  <NodeKeyPoolsPanel />
</KeyUtilsProvider>

Each provider creates a .sn-keyutils scope. Colors, reset rules, typography, utilities, and animations stay inside that scope; the package does not toggle classes on the document. KeyUtilsTheme exposes the same wrapper for standalone presentational components. Providers and theme wrappers accept className and style for host layout. Nested wrappers may use different themes.

The widget uses full-width segmented navigation and the original max-w-xl content width (36rem), with individual bordered key cards and Active/Listed filters. Meters show tokens remaining in 40 segments: green at 60% or more, amber at 30–59%, and red below 30%. Numeric values remain visible alongside color. Controls include keyboard focus styles, and animation respects reduced motion.

Font assets, OFL licenses, and source versions/checksums are included in dist/fonts. The fonts are served from the installed package alongside the CSS.

Components

BuyKeyWidget

Main embeddable widget with Buy, Use, and Keys tabs. Supplying pools adds a Pools tab.

| Prop | Type | Description | |------|------|-------------| | apiBaseUrl | string | Base URL for API calls (use "" for same-origin) | | stackId | string | Stack identifier for payment config | | stackName | string | Display name for the originating stack | | paymentMethods | ('stripe' \| 'solana')[] | Enabled payment methods | | merchantWallet | string | Solana wallet address for crypto payments | | maxQuantity | number | Max keys per purchase (default: 5) | | keyImage | string | Image URL shown above price on Buy tab | | keyImageClass | string | CSS class for key image (default: w-1/2 mx-auto) | | aispImage | string | Image URL shown on Use tab | | termsUrl | string | URL for terms link (default: /terms) | | solanaRpcUrl | string | Solana RPC endpoint (default: mainnet) | | defaultTab | 'buy' \| 'use' \| 'keys' \| 'pools' | Initial active tab (pools requires pool configuration) | | theme | 'dark' \| 'light' \| 'system' | Color theme (default: dark) | | pools | NodeKeyPoolsConfig | Enables pool creation, invitations, participation, and management | | onMintSuccess | (result) => void | Called after successful key generation | | onMintError | (error) => void | Called on generation failure | | onListingCreated | (listing) => void | Called after marketplace listing |

KeyUtilsProvider

Context provider for using individual components and hooks outside of BuyKeyWidget.

import { KeyUtilsProvider, KeyList } from '@stacknet/keyutils/components';

<KeyUtilsProvider config={{ apiBaseUrl: '' }}>
  <KeyList getKeyHref="/keys/get" />
</KeyUtilsProvider>

Other Components

| Component | Description | |-----------|-------------| | BuyPanel | Purchase flow with pricing, quantity, and payment | | UsePanel | Download links with platform detection | | SellPanel | Marketplace listing interface | | KeyList | Displays user's keys with balances | | KeyCard | Single key with expandable ledger | | KeyBalanceBar | Token balance progress bar | | SolanaPayButton | Solana payment via Phantom wallet | | MintSuccess | Post-purchase success display |

All components except BuyKeyWidget and NodeKeyPoolsWidget require a parent KeyUtilsProvider.

Node key pools

Pools delegate operating rights only: arm, disarm, and node operation. The original owner retains the key, earnings, and spending permissions. A controller cannot transfer, list, sell, withdraw, or redelegate a contributor's key. An invitation identifies a pool; it grants no permission. Operating failures remain subject to protocol slashing.

Add the Pools tab or embed it separately

import { BuyKeyWidget, NodeKeyPoolsWidget } from '@stacknet/keyutils/components';
import type { NodeKeyPoolsConfig } from '@stacknet/keyutils/types';

// This example assumes the host implements a trusted /api/stacknet proxy
// for the /v1 routes listed below. Existing /api/keys routes are insufficient.
const pools: NodeKeyPoolsConfig = {
  apiBaseUrl: '/api/stacknet',
  inviteBaseUrl: 'https://your-stacknet-authority.example',
};

<BuyKeyWidget apiBaseUrl="" pools={pools} defaultTab="pools" />

// Or a self-contained pool-only embed:
<NodeKeyPoolsWidget config={pools} />

Without signCommand, users can view their pools, copy invitations, use their account-authorized owner enable switch, and follow the displayed CLI commands for signed operations. With a signer connected, forms support creating a pool, reviewing and signing a contribution, updating its expiry/reserve by joining the same pool again, controller arm/disarm, and owner revocation. Creation requires an active owned key and the public key of an existing controller signer. It does not automatically delegate the creator's key.

NodeKeyPoolsPanel also works inside KeyUtilsProvider with config.pools. Panel and standalone widget props:

| Prop | Purpose | |------|---------| | initialPoolId | Initially selected pool, subject to account access | | invitation | Prefills the join form with an ID or URL; always requires review and explicit consent | | onPoolChanged(poolId) | Called after an accepted mutation | | className, style | Host styling |

Connect the existing signer

The host provides a NodeKeyPoolSigner callback. It receives the required role, expected public key, the command to review, and exact UTF-8 bytes to sign with Ed25519. It returns a canonical base64 encoding of the 64-byte signature. No private key, seed, or wallet dependency is accepted or stored by keyutils.

import type { NodeKeyPoolSigner } from '@stacknet/keyutils/types';

// Implement this adapter using your existing signer integration.
// signerBridge here is host-owned; it is not an export from this package.
const signCommand: NodeKeyPoolSigner = async (request) => {
  const signer = await signerBridge.select({
    role: request.role,
    publicKeyB64: request.publicKeyB64,
    keyId: request.keyId,
    poolId: request.poolId,
  });
  if (signer.publicKeyB64 !== request.publicKeyB64) {
    throw new Error('The connected signer does not match this key or pool');
  }
  return signer.reviewAndSignEd25519(request.command, request.payload);
};

const pools = {
  apiBaseUrl: '/api/stacknet',
  inviteBaseUrl: 'https://your-stacknet-authority.example',
  signCommand,
};

Owner signatures use the node key's already bound Ed25519 identity; an unrelated payment wallet cannot sign for it. If that binding is absent, the UI reports it and the CLI delegation workflow can initialize the local identity. Pool creation stores an immutable controller public key: provision and retain its matching signer on node hosts before creating the pool. The package does not generate controller keys or implement controller recovery. Treat the callback's command as review data; sign the supplied bytes exactly.

Client and hook

import { createNodeKeyPoolClient } from '@stacknet/keyutils/core';

const client = createNodeKeyPoolClient({
  apiBaseUrl: 'https://your-stacknet-authority.example',
  getHeaders: () => auth.getHeaders(), // host-owned account auth
  signCommand,                        // host-owned signer adapter above
});

const { pools } = await client.list();
const { keys, ownerId } = await client.getOwnedKeys();
const details = await client.getDescriptor('nkp_pool_id');
await client.delegate({
  keyId: 'nk_key_owned_id',
  pool: details.poolId,
  descriptor: details, // pin the pool descriptor the user reviewed
  expiresInDays: 30,
  minBalance: '1000',
});
await client.setOwnerEnabled('nk_key_owned_id', true);
// Separately, with the controller's signer:
await client.setPoolArmed('nkp_pool_id', 'nk_key_owned_id', true);
// The key owner may revoke at any time:
await client.revoke('nk_key_owned_id');

create({ name, creatorKeyId, operatorPubkeyB64, poolId? }), getStatus(poolId), and invitation(poolId) complete the client API. Read methods accept an optional AbortSignal. NodeKeyPoolError.status preserves HTTP status codes, including stale-nonce conflicts (409). Mutations fetch fresh authoritative key state before requesting a signature, and do not automatically retry or re-sign a failed request. Nonces, owner epochs, and reserves remain decimal strings without number coercion. A two-minute signing request that expires while awaiting approval is not submitted.

useNodeKeyPools(poolId?) exposes client, pools, keys, ownerId, status, loading, error, actionError, busy, canSign, refresh(), and run(client => client.operation(...)). run serializes user actions and refreshes authoritative state, returning the result on success or null on failure. Errors are available separately. Mount it inside KeyUtilsProvider with config.pools. Keep auth/signer callbacks stable; remount the widget on an account change or supply new callbacks so account-specific state is cleared.

Pool transport routes

pools.apiBaseUrl is the authority or trusted proxy root, without /v1. All of these routes must reach the same authority:

| Route | Method | Authorization | |-------|--------|---------------| | /v1/node-key-pools | GET | Account; lists created or contributed pools and the signing domain | | /v1/node-key-pools/:id | GET | Account; creator sees operational roster, contributor sees own keys | | /v1/node-key-pools/:id/descriptor | GET | Public; minimum enrollment descriptor | | /v1/node-key-pools/:id/join | GET | Public invitation page (link destination) | | /v1/node-key-pools/commands | POST | Protocol owner or controller signature | | /v1/node-keys/by-owner | GET | Account; owned keys, signer binding, nonce and pool state | | /v1/node-keys/:id/arm | POST | Owner account with write permission; { armed: boolean } |

For browser integration, use same-origin authenticated proxies or configure the authority's CORS and account transport. getHeaders supports bearer/native authentication. Browser mutations include the existing CSRF header; a cookie-authenticated host proxy must enforce CSRF and authenticate its owner routes. Proxy signed commands unchanged to the authority; do not replace a contributor signature with an application/admin signer or translate the operator into a key owner. The authority enforces the final permission check. No Next.js routes are installed by this library.

Use inviteBaseUrl for the public authority root when requests use a proxy. Invitations are restricted to that configured origin and path; the client extracts only the ID and never follows an invitation host with account credentials. Descriptors omit credentials, API redirects are rejected, and expectedDomain can pin a known authority signing domain. Pool-list and descriptor domains must match before signing.

Participation and rotation

Delegate for 1–90 days with at least 1,000 tokens reserved. Drain a running node before delegation; the authority rejects an active serving lease or competing live worker session. New grants start with the pool switch disarmed. The owner enable switch and controller pool switch are independent, and both must be on. The owner can revoke immediately; this can interrupt in-flight work. Expired delegations remain visible until explicitly revoked or replaced.

The UI displays operational status, reserves, expiry, and active worker sessions as a snapshot. Refresh for current state. Automatic acquisition, renewal, draining, and rotation run in the existing CLI supervisor:

stacknet node start --pool nkp_pool_id --relay

The default rotation low-water threshold is 10,000 tokens; a replacement must clear that threshold and the owner's reserve. An Available UI label reports basic eligibility; the authority arbitrates actual acquisition, and the supervisor applies its configured threshold. Browser tabs never acquire worker sessions or run a hidden rotation loop. No reward sharing, ownership custody, controller rotation, or pool deletion is introduced.

Pool components use the bundled stylesheet and the same scoped theme as the rest of keyutils.

Hooks

import { usePricing, useKeys, useMintKey, useListKey } from '@stacknet/keyutils/hooks';

| Hook | Returns | Description | |------|---------|-------------| | usePricing() | { pricing, loading, error, refresh } | Live pricing with 60s auto-refresh | | useKeys() | { keys, totalBalance, loading, error, refresh } | User's node keys | | useKeyLedger(keyId) | { entries, loading, error, refresh } | Ledger for a specific key | | useMintKey() | { mint, minting, result, error } | Key generation after payment | | useListKey() | { listKey, delistKey, listing, result, error } | Marketplace list/delist | | useStripeCheckout() | { createSession, loading, error } | Stripe Checkout Session creation |

All hooks require a parent KeyUtilsProvider.

API Proxy Routes

The widget expects these proxy routes on the host app:

| Route | Method | Proxies To | |-------|--------|------------| | /api/keys | GET | /api/v2/node-keys | | /api/keys/pricing | GET | /api/v2/node-keys/pricing | | /api/keys/mint | POST | /api/v2/node-keys/mint | | /api/keys/:id | GET | /api/v2/node-keys/:id | | /api/keys/:id/ledger | GET | /api/v2/node-keys/:id/ledger | | /api/keys/:id/list | POST/DELETE | /api/v2/node-keys/:id/list | | /api/keys/stripe-session | POST | /api/v2/node-keys/stripe-session |

Sub-path Exports

import { BuyKeyWidget } from '@stacknet/keyutils/components';
import { usePricing } from '@stacknet/keyutils/hooks';
import type { NodeKeyInfo } from '@stacknet/keyutils/types';
import { cn, formatTokens } from '@stacknet/keyutils/utils';

Build

pnpm --filter @stacknet/keyutils build
pnpm --filter @stacknet/keyutils typecheck
pnpm --filter @stacknet/keyutils test
pnpm --filter @stacknet/keyutils test:exports
pnpm --filter @stacknet/keyutils test:styles

test:exports runs after building and checks ESM/CJS exports, React client boundaries, and shared provider context across /components, /hooks, and root imports.

test:styles checks the published CSS export, selector and animation scoping, theme rules, local font assets, checksums, and licenses. Both regular and publish builds generate the stylesheet and copy its fonts.

License

MIT

SOL payment signing

SolanaPayButton and useSolanaPayment require a wallet with signTransaction support. They verify that solanaRpcUrl is an HTTPS mainnet RPC before asking for a signature, then verify the network again before broadcasting and confirming through that RPC. Custom HTTPS proxy paths and API-key query parameters are supported. A changed transaction or invalid signature is rejected before broadcast.