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

@tempoxyz/horcrux-viem

v0.1.1

Published

A viem account backed by Horcrux remote transaction signing

Readme

Horcrux viem account

An ESM TypeScript package for Node.js 22+, tested with viem 2.56.8. Accounts accept either a wallet UUID or EVM address. The transaction account signs through POST /v1/wallets/{id}/sign. viem handles transaction preparation and chain RPC. No Privy credentials belong in the consuming service.

See the runtime agent guide and generated OpenAPI for the server contract and recovery rules. This adapter supports EIP-1559 and Tempo, not the server's Solana/Tron variants.

Build and test

From this directory:

npm ci
npm run build
npm run check
npm test
npm run test:integration

Formatting and linting follow viem 2.56.5's Biome configuration: two-space indentation, single quotes, and no optional semicolons. Run npm run format to apply fixes; npm run check builds the package and checks style and types (including the integration test's public package import).

After the first public release, install with npm install @tempoxyz/horcrux-viem [email protected]. Before release, build and install from a local checkout with npm install /path/to/horcrux/sdk/viem [email protected].

The integration command builds the package and explicitly runs both ignored Rust HTTP tests. Each starts the real Axum router with an in-memory database, test-only authentication, and a mock Privy HTTP server. Tests import the built package and check EIP-1559 and native Tempo signatures/fields, a lost HTTP response, cached replay, conflicting key reuse, and unauthenticated access. Rust assertions verify that Privy was called only once and exactly three state events were saved per operation. They use no live wallet, credentials, funds, or chain RPC. CI runs these tests with --include-ignored; a plain cargo test does not run them.

Tempo sponsorship account

await horcruxTempoAccount(...) discovers the wallet address through Horcrux and returns an address-pinned viem LocalAccount; no separate address configuration is needed. Alternatively, horcruxTempoAccount({ address, ... }) returns the account synchronously without any discovery request. horcruxAccount supports the same two modes. Supply exactly one of walletId or address. It uses /v1/wallets/{id}/sign-hash for relay raw signing, Tempo fee-payer transactions (including MPP), and ordinary Tempo sender transactions. Applications only construct the account and pass it to their existing viem integration:

import { horcruxTempoAccount } from '@tempoxyz/horcrux-viem'

const account = await horcruxTempoAccount({
  url: env.HORCRUX_URL,
  walletId: env.HORCRUX_FEE_PAYER_WALLET_ID,
  auth: {
    type: 'cloudflare-access',
    clientId: env.HORCRUX_ACCESS_CLIENT_ID,
    clientSecret: env.HORCRUX_ACCESS_CLIENT_SECRET,
  },
  onError: (error) => reportSigningFailure(error),
})

For address mode, replace walletId above with address: env.HORCRUX_FEE_PAYER_ADDRESS and remove await. Horcrux resolves the address during signing; grants, policies and recovery remain scoped to the canonical wallet UUID. Ambiguous addresses fail closed, including inactive or ungranted duplicates. Address mode requires a server with address-reference support; there is no discovery fallback. Deploy the server migration before adopting address mode.

Initialize UUID-based accounts in a request-capable context, not at Worker module scope. Discovery validates the requested wallet ID, active status, EVM family and address before returning. A discovery failure makes no signing request and is reported as WALLET_DISCOVERY_FAILED, without a signing recovery key.

Signing uses one POST, with no pre-sign GET. The server checks grants and active status before replay or dispatch and verifies the provider binding at dispatch. The SDK verifies the returned signature against the exact digest and pinned signer, and preserves the sender's signature when adding sponsorship. It rejects bigint validity timestamps before serialization; use numeric Unix seconds. HTTPS is required except on loopback. It never funds, broadcasts, falls back to a key, or signs personal messages/typed data. The caller remains responsible for transaction authorization, fee limits and storage. Raw-hash policies cannot inspect transaction fields.

Unlike horcruxAccount, this account makes no automatic transport retry. Its digest-derived idempotency key survives account reconstruction and matches the original Tempo API adapter (tempo-api:hash:v1: plus the keccak256 of the canonical JSON hash request). Keep the exact original transaction when recovering; changing fees, chain, nonce, sender or token produces a different digest and operation. onError receives a sanitized HorcruxError with the recovery key and available request ID/status/code. Reconcile unknown outcomes before explicitly replaying.

Publishing

Releases use npm's trusted publishing from .github/workflows/publish-sdk.yml. No npm publishing token is stored in GitHub. The workflow runs only by manual dispatch on main, requires successful CI for the exact commit, checks the requested version, and rebuilds/tests the SDK. The publish job uses the npm-publish GitHub environment, restricted to the main branch. No environment reviewer approval is currently required. Only dist, package metadata, and this README are packed; prepack builds fresh JavaScript and declarations for both local packing and publishing.

An npm organization owner must complete the initial setup before CI can publish:

  1. Confirm publishing rights to the npm @tempoxyz scope and the intended public name @tempoxyz/horcrux-viem.
  2. If the package does not exist, bootstrap its first version from a reviewed main checkout after CI passes. In sdk/viem, run npm ci, npm run check, npm test, and npm pack --dry-run; inspect the file list, then use an authorized npm login with 2FA to run npm publish. This is a public release.
  3. In the package's npm Settings → Trusted publishing, add GitHub Actions with organization tempoxyz, repository horcrux, workflow filename publish-sdk.yml, and environment name npm-publish. Allow direct npm publish, not only staged publishing. These settings live on npm, not in this repository.
  4. Once OIDC publishing works, select “Require two-factor authentication and disallow tokens” in npm's publishing access settings.

For subsequent releases, update the SDK version and lockfile together with npm version <version> --no-git-tag-version in this directory and merge the PR. After CI passes on main, dispatch Publish SDK with that exact version:

gh workflow run publish-sdk.yml --repo tempoxyz/horcrux --ref main -f version=0.1.1

The version is an example, not an automatic bump. An already-published version cannot be overwritten. Use GitHub-hosted runners: npm OIDC does not support the Depot runners used by regular CI. Public packages from this private repository can use OIDC, but npm does not generate provenance for private source repositories. This workflow neither deploys Horcrux nor changes repository visibility.

Authentication

Configure the server's authentication listeners first. Without authentication configuration, the binary rejects /v1 requests. For deployed services, set auth to either KubernetesAuth or CloudflareAccessAuth. Its type selects the method; fields from the two methods cannot be mixed. The Kubernetes getToken callback runs once per operation; Cloudflare credentials are passed directly. A transport retry uses the same credentials, body, and request key.

For Kubernetes, read the projected service-account token each time so automatic replacement is picked up. The mounted token must have the configured Horcrux audience, not the default Kubernetes API audience:

import { readFile } from 'node:fs/promises'
import { horcruxAccount } from '@tempoxyz/horcrux-viem'

const account = await horcruxAccount({
  url: 'https://horcrux.internal.example.com',
  walletId: process.env.HORCRUX_WALLET_ID!,
  auth: {
    type: 'kubernetes',
    getToken: async () =>
      (await readFile('/var/run/secrets/horcrux/token', 'utf8')).trim(),
  },
})

For Workers, store a Cloudflare Access service token's Client ID and Client Secret in Worker secrets. Pass the Worker environment's values directly:

const account = await horcruxAccount({
  url: 'https://horcrux.example.com',
  walletId: env.HORCRUX_WALLET_ID,
  auth: {
    type: 'cloudflare-access',
    clientId: env.CF_ACCESS_CLIENT_ID,
    clientSecret: env.CF_ACCESS_CLIENT_SECRET,
  },
})

This sends CF-Access-Client-Id and CF-Access-Client-Secret, without an Authorization header. Cloudflare validates the credentials; Horcrux validates Cloudflare's signed assertion. Do not send raw credentials directly to the origin and expect them to authenticate. Replacing a secret does not change the service's identity or wallet access. Do not log credentials or request headers.

Local development

On your development machine, set DATABASE_URL to a fresh disposable PostgreSQL database, then run cargo run --locked --example local_auth from the repository root. Discard this example database when finished; never use an environment's application database. Then use:

const account = await horcruxAccount({
  url: 'http://127.0.0.1:3040',
  walletId: '4396bb1d-a4ef-4ad0-b6d5-9388bc796270',
  auth: { type: 'none' },
})

This explicitly sends no credentials. It is restricted to loopback hostnames, does not bypass a server's authentication, and cannot be combined with credentials. The example uses one fixed identity and a disposable wallet. It supports discovery and API validation but returns 503 SIGNING_NOT_CONFIGURED for signing; it does not read Privy secrets or broadcast. Never expose it through a proxy or tunnel. For fully isolated tests, use the existing fetch option to supply mock responses.

Real Kubernetes authentication test

The ordinary Rust auth tests use synthetic signed tokens. Enable the kubernetes-integration Cargo feature to compile the ignored kubernetes_auth_over_http test. It uses real Kubernetes discovery, keys, HTTPS certificates, and tokens, then runs this SDK over HTTP against the normal Horcrux authentication middleware. PostgreSQL is disposable and Privy is mocked. Set DATABASE_URL to the disposable instance described in the root README.

Tested with k3s v1.36.4+k3s1 and a host-network pod with a projected token. The SDK runs outside the pod, reading a private copy of that token. File replacement uses a second real TokenRequest token; this checks SDK rereading, not kubelet's timed rotation. Expiry and other malformed claims remain covered by synthetic-token tests.

Use only a disposable single-node cluster. The manifest creates payments service accounts and grants anonymous read access to the public token-discovery endpoints; do not apply it to an existing shared cluster. For the localhost k3s recipe, start the server with these additional arguments:

--tls-san 127.0.0.1 \
--kube-apiserver-arg anonymous-auth=true \
--kube-apiserver-arg service-account-issuer=https://127.0.0.1:6443 \
--kube-apiserver-arg service-account-jwks-uri=https://127.0.0.1:6443/openid/v1/jwks

Point KUBECONFIG at that cluster and run from the repository root. Set K3S_DATA_DIR to its data directory (normally /var/lib/rancher/k3s):

umask 077
export HORCRUX_KUBERNETES_TEST_DIR="$PWD/.local/kubernetes-auth-test"
mkdir -p "$HORCRUX_KUBERNETES_TEST_DIR"
kubectl apply -f examples/kubernetes-auth.yaml
kubectl wait -n payments pod/horcrux-auth-test --for=condition=Ready --timeout=120s
sudo cat "$K3S_DATA_DIR/server/tls/server-ca.crt" > "$HORCRUX_KUBERNETES_TEST_DIR/ca.pem"
sudo cat "$K3S_DATA_DIR/server/tls/client-ca.crt" > "$HORCRUX_KUBERNETES_TEST_DIR/wrong-ca.pem"
printf '%s\n' 'https://127.0.0.1:6443' > "$HORCRUX_KUBERNETES_TEST_DIR/issuer.txt"
kubectl exec -n payments horcrux-auth-test -- cat /var/run/secrets/horcrux/token > "$HORCRUX_KUBERNETES_TEST_DIR/valid.token"
kubectl create token settlement -n payments --audience=horcrux --duration=10m --bound-object-kind=Pod --bound-object-name=horcrux-auth-test > "$HORCRUX_KUBERNETES_TEST_DIR/rotated.token"
kubectl create token settlement -n payments --audience=not-horcrux --duration=10m > "$HORCRUX_KUBERNETES_TEST_DIR/wrong-audience.token"
kubectl create token unrelated -n payments --audience=horcrux --duration=10m > "$HORCRUX_KUBERNETES_TEST_DIR/other-account.token"
npm --prefix sdk/viem run build
cargo test --locked --features kubernetes-integration kubernetes_auth_over_http -- --ignored --nocapture

This checks valid identity, token-file replacement, wrong audience, tampered signature, other-account wallet denial, missing/wrong CA rejection, exact signed bytes, and a lost-response retry making only one call to mock Privy. Regenerate tokens before rerunning after ten minutes. Remove the test resources with kubectl delete -f examples/kubernetes-auth.yaml, stop the disposable cluster, and delete the private token directory afterward. Never commit tokens or kubeconfigs.

Sign a pathUSD transfer on Moderato

Use the authentication option appropriate to the configured server listener. The example below uses a bearer provider; in a pod, prefer the file-reading provider above instead of copying a short-lived token into an environment variable.

import { createWalletClient, encodeFunctionData, http, parseAbi, parseUnits } from 'viem'
import { tempoModerato } from 'viem/chains'
import { horcruxAccount } from '@tempoxyz/horcrux-viem'

const account = await horcruxAccount({
  url: 'https://horcrux.example.com',
  walletId: process.env.HORCRUX_WALLET_ID!,
  auth: {
    type: 'kubernetes',
    // Replace with the service's token provider; called for each operation.
    getToken: () => process.env.HORCRUX_ACCESS_TOKEN!,
  },
})

const client = createWalletClient({
  account,
  chain: tempoModerato,
  transport: http('https://rpc.moderato.tempo.xyz'),
})

const pathUSD = '0x20c0000000000000000000000000000000000000'
const prepared = await client.prepareTransactionRequest({
  type: 'tempo',
  feeToken: pathUSD,
  calls: [{
    to: pathUSD,
    value: 0n,
    data: encodeFunctionData({
      abi: parseAbi(['function transfer(address recipient, uint256 amount) returns (bool)']),
      functionName: 'transfer',
      args: ['0x91000F2d39017A0E4403D75f2CC977d2A9B77B61', parseUnits('1', 6)],
    }),
  }],
})

const signed = await client.signTransaction(prepared)
// Signed bytes only. Nothing has been broadcast.
// client.sendTransaction(...) would also broadcast via the configured chain RPC.

Transaction account operations and failures

The following applies to the original asynchronous horcruxAccount, not the raw-digest horcruxTempoAccount above.

  • EIP-1559 transactions, with an empty access list and a destination address.
  • Native Tempo on mainnet or Moderato: one or two calls, zero native value, an explicit fee-token address, nonce lane, and optional validity bounds in Unix seconds. viem's gas becomes Horcrux's gasLimit; bigint quantities become hex strings.
  • Call prepareTransactionRequest before signTransaction if nonce, gas, or fees are missing. sendTransaction performs preparation itself.
  • Message signing, typed-data signing, arbitrary hash signing, authorization signing, sponsorship, batches larger than two calls, and contract creation are not supported. Unsupported transaction fields are rejected before a signing HTTP request.
  • HTTP and HTTPS URLs are supported on any host. HTTP sends credentials unencrypted; use HTTPS in production. Redirects are not followed; a custom fetch implementation must preserve that behavior.

Each signing invocation creates a fresh idempotency key. A lost connection or response body gets one retry with the same key and serialized body. Each HTTP attempt has a 20-second timeout. HTTP errors are not retried, including 409 (SIGNING_IN_PROGRESS) and 502 (SIGNING_UNRESOLVED or PROVIDER_REJECTED). Access may return HTML or an empty rejection before Horcrux sees a request. These failures retain the HTTP status and recovery key as HTTP_ERROR, without copying the response body into the error. They are not retried.

HorcruxError exposes status, code, requestId, and idempotencyKey when available. Retain these for reconciliation. Do not blindly call signTransaction again after an uncertain outcome: that creates a new operation and key. There is no polling or cross-process retry journal in this first version. viem may wrap this error as the cause of its own error.

The adapter trusts Horcrux to validate the signature and transaction; it checks the response structure and hex encoding. Horcrux rechecks wallet access on each request, including cached results.