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

@crypto-signerkit/server

v0.4.0

Published

Server SDK for SignerKit — verify auth, manage action-bound step-up intents, and sign within delegated policy.

Readme

@crypto-signerkit/server

Server SDK for SignerKit — verify login tokens from your frontend, look up users, and sign for users within a delegated policy. The client is configured with your public App ID (pk_) and server-only App Secret (sk_) for online API calls. Offline token verification has a different boundary, described below.

Current public contract: 0.4.0.

Install

npm i @crypto-signerkit/server

Usage

import { SignerKitClient } from '@crypto-signerkit/server'

const signerkit = new SignerKitClient({
  apiUrl: 'https://api.signerkit.com',
  appId: 'pk_...',
  appSecret: 'sk_...',   // server-side only — never ship to the browser
})

// Verify locally against JWKS (the default resolver fetches/caches public JWKS as needed)
const { userId, appId: internalAppId } =
  await signerkit.verifyAuthToken(tokenFromFrontend)

// Look up a user + their wallets
const user = await signerkit.getUser(userId)
const byAddr = await signerkit.getUserByWalletAddress('0x...')

// Automation: sign for a user within a policy you were granted, without holding the key
const signature = await signerkit.walletApi.signDelegated({
  delegationId, chain, kind, payload, authzPrivKey,
})

Token and app boundaries

verifyAuthToken verifies the JWT signature, issuer, audience, access-token marker, expiry, and required tenant claims. It does not make an app-authenticated authorization call or use the configured App Secret as a second check. Unless you inject a static JWKS, the default resolver may fetch and cache SignerKit's public JWKS. The returned appId is the internal app UUID from the JWT app_id claim; it is not the public pk_ value in SignerKitClient options, so do not compare those two values.

The public App ID and x-signerkit-app-id header are app locators, not secrets or additional authentication factors. If your backend accepts ordinary login tokens for only one sibling app, compare the verified internal UUID with that app's internal UUID from your trusted configuration. Refresh sessions remain app-bound.

Step-up has a stronger online contract: its user-facing ceremony checks the exact app and active session, while backend create/consume calls require the matching app credentials. A sibling app mismatch is rejected. This exact step-up binding does not turn the public App ID itself into a credential.

One-time step-up intents

For a sensitive business operation, the backend creates an intent bound to its own authoritative request fields. The browser verifies the intent with the server-selected email or TOTP method, then the backend consumes it online with the same fields:

Step-up proves recent reauthentication, not informed consent to transaction details. Before starting the ceremony, load the pending operation from your backend and show its exact human-readable action, resource, amount, destination, network, and fees (as applicable) in your own trusted confirmation UI. Hash the same authoritative operation details plus an immutable operation ID into the binding. If any displayed field changes, abandon that intent and create a new one. The public intent and generic OTP email intentionally omit these details, so neither an OTP prompt nor successful consumption is approval by itself.

import { SignerKitClient, hashStepUpBinding } from '@crypto-signerkit/server'

const binding = hashStepUpBinding({ operationId, requestedEmail })
const intent = await signerkit.createStepUpIntent({
  accessToken,
  expectedUserId,
  action: 'account.email.change',
  binding,
  minimumAssurance: 'reauth',
  idempotencyKey: creationAttemptId,
})

// Send only intent.intentId to the authenticated browser. After its ceremony:
const proof = await signerkit.consumeStepUpIntent({
  intentId: intent.intentId,
  expectedUserId,
  action: 'account.email.change',
  binding,
  minimumAssurance: 'reauth',
  idempotencyKey: operationId,
})

// Consume is necessary, but not sufficient authorization.
await recheckAuthorization({ actorId: proof.userId })
await changeEmailIdempotently({ operationId, requestedEmail })

Creation requires both the user's access token and the app credentials. Consumption requires app credentials and is one-time: the same consume key replays the same result, while a different key is rejected. SignerKit does not replace resource ownership checks or business-operation idempotency. hashStepUpBinding applies the SignerKit domain separator and RFC 8785 JSON canonicalization so both calls use an exact, stable SHA-256 binding. minimumAssurance: 'reauth' selects email OTP for a user without TOTP and selects the stronger TOTP method when one is enrolled. Use 'mfa' only after the staged TOTP enrollment flow; it never falls back to email.

action must be 1–100 characters and match [a-z0-9][a-z0-9._:-]*. Each idempotency key must be 16–200 printable ASCII characters. Persist both keys with the pending operation: use one stable key for creation retries and a separate stable business-operation key for consume.

Creation can commit before the caller receives a response. Keep the creation idempotency key with the pending business operation and retry with that same key after an unknown network outcome or email_delivery_pending; never mint a new key just because a response was lost. SignerKitError exposes the stable code, HTTP status, optional retryAfter seconds, and—when the API received the request—an optional intentId. Honor retryAfter. Treat email_delivery_failed as a failed ceremony and start a genuinely new business attempt only through your normal user-confirmed flow.

Irreversibly remove a wallet (0.4.0)

Wallet removal uses a reserved, atomic workflow. It is irreversible and has no restore API: after success, the wallet is removed from SignerKit, disappears from public wallet reads, and can no longer sign. In every deployed environment, SignerKit permanently retains the wallet tombstone, its HD seed or imported ciphertext, and canonical deletion facts. Removal is not cryptographic key destruction or data erasure.

The App Secret and deleteWallet call are server-side only. The authenticated browser sends its current access token to your backend over your own protected channel; do not log or persist that token. The complete flow is:

// 1) BROWSER — use your authenticated backend channel.
const { getAccessToken } = useSignerKit()
const accessToken = await getAccessToken()
await backend.startWalletRemoval({
  walletId,
  accessToken,
})

// 2) SERVER — before the first request, transactionally insert-or-read your
// pending operation and both stable keys. This partner-owned function must
// durably commit before it returns; on conflict it returns the existing row.
import { randomBytes } from 'node:crypto'
import {
  SignerKitClient,
  hashWalletDeletionBinding,
} from '@crypto-signerkit/server'

// This is your immutable, server-authoritative business-operation ID.
const removal = await loadOrCreateWalletRemoval({
  operationId: walletRemovalOperationId,
  walletId,
  create: () => ({
    createIdempotencyKey:
      'wallet-delete-create:' + randomBytes(16).toString('base64url'),
    deleteIdempotencyKey:
      'wallet-delete:' + randomBytes(16).toString('base64url'),
  }),
})

// 3) SERVER — verify the token, derive its user ID, then load the wallet identity.
const { userId } = await signerkit.verifyAuthToken(accessToken)
const user = await signerkit.getUser(userId)
const wallet = user?.wallets.find((candidate) => candidate.id === walletId)
if (!user || !wallet) throw new Error('wallet not found')
const binding = hashWalletDeletionBinding({ userId, wallet })

// 4) SERVER — the access token proves the user; configured App credentials
// authenticate this backend request. `reauth` selects Email OTP or enrolled TOTP.
const intent = await signerkit.createStepUpIntent({
  accessToken,
  expectedUserId: userId,
  action: 'wallet.delete',
  binding,
  minimumAssurance: 'reauth',
  idempotencyKey: removal.createIdempotencyKey,
})
await walletRemovalStore.recordIntent(removal.operationId, intent.intentId)
// Return only intent.intentId to the same authenticated browser session.

// 5) BROWSER — complete the existing Step-up ceremony.
const { verify } = useStepUp(intent.intentId)
await verify(code)

// 6) SERVER — reload the same durable row on every finish/retry request.
const readyRemoval =
  await walletRemovalStore.loadRequired(walletRemovalOperationId)
const deleted = await signerkit.deleteWallet({
  walletId: readyRemoval.walletId,
  intentId: readyRemoval.intentId,
  idempotencyKey: readyRemoval.deleteIdempotencyKey,
})

// 7) BROWSER — refresh only after backend success.
const { refresh } = useWallets()
await refresh()

The create and delete idempotency keys are separate, stable request IDs. Each must contain at least 128 bits of cryptographic randomness, be 16–200 printable ASCII characters, and remain non-secret and non-PII. loadOrCreateWalletRemoval is illustrative partner storage, not an SDK method: implement it as an atomic insert-or-read whose commit finishes before the first createStepUpIntent call. Persist the returned intent ID before giving it to the browser. A same-key replay of the same delete request returns the original deletedAt with replayed: true; reusing the key for another wallet/intent returns idempotency_conflict, while retrying a consumed intent under a different key is rejected.

wallet.deleted is an opt-in, merchant-wide webhook: subscribe every active app that must invalidate its wallet cache. The owner security notice uses durable at-least-once delivery and can very rarely duplicate after recovery beyond the email provider's idempotency window.

Full docs: https://signerkit.com.

License

MIT