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

@openzeppelin/guardian-operator-client

v0.15.0

Published

TypeScript HTTP client for Guardian operator dashboard endpoints

Readme

@openzeppelin/guardian-operator-client

TypeScript HTTP client for Guardian operator dashboard endpoints.

Installation

npm install @openzeppelin/guardian-operator-client

Setup

import { GuardianOperatorHttpClient } from '@openzeppelin/guardian-operator-client';

const client = new GuardianOperatorHttpClient({
  baseUrl: 'http://localhost:3000',
  credentials: 'include',
});

Usage

Request A Challenge

const challenge = await client.challenge('0x...');
console.log(challenge.challenge.signingDigest);

Verify A Signed Challenge

The package does not talk to wallets or sign challenges. Callers provide the commitment and signature.

const verified = await client.verify({
  commitment: '0x...',
  signature: '0x...',
});

console.log(verified.operatorId);

List Accounts (Paginated)

// Default page size is 50; max is 500.
const page = await client.listAccounts({ limit: 50 });
console.log(page.items[0]?.accountId);

// Resume the next page with the cursor from the previous response.
if (page.nextCursor !== null) {
  const next = await client.listAccounts({
    limit: 50,
    cursor: page.nextCursor,
  });
  console.log(next.items.length);
}

// Filter by pause state. Omit `paused` for all accounts.
const pausedOnly = await client.listAccounts({ paused: true });
const activeOnly = await client.listAccounts({ paused: false });

Fetch One Account

getAccount() returns the bare DashboardAccountDetail — there is no { success, account } wrapper. Read fields directly on the response.

const detail = await client.getAccount('0x...');
console.log(detail.authorizedSignerIds);
console.log(detail.currentCommitment, detail.stateUpdatedAt);

Decoded Account Snapshot

Returns the decoded state (vault contents, pending-candidate flag) at the commitment Guardian last canonicalized for the account. Sourced from Guardian's stored state — no live Miden RPC calls.

const snapshot = await client.getAccountSnapshot('0x...');
console.log(snapshot.commitment, snapshot.updatedAt);
if (snapshot.hasPendingCandidate) {
  console.warn('vault may be stale — candidate delta in flight');
}
for (const asset of snapshot.vault.fungible) {
  console.log(asset.faucetId, asset.amount);
}

Inventory And Health Snapshot

const info = await client.getDashboardInfo();
console.log(info.totalAccountCount, info.environment);
console.log(info.deltaStatusCounts.candidate);
console.log(info.inFlightProposalCount);
if (info.serviceStatus === 'degraded') {
  console.warn('degraded aggregates:', info.degradedAggregates);
}

Per-Account Delta Feed

Each entry carries the dashboard-ready activity fields spread directly at L1 (feature 007). category is the server-curated coarse classification; assets (array) / counterparty / noteCounts are derived from the on-chain TransactionSummary; proposalType echoes the operator's intent label for multisig commits. All five fields are optional — absent fields mean "not derivable from this row" (EVM deltas, pre-feature-007 historical rows whose proposal was already deleted, or undecodable payloads).

const page = await client.listAccountDeltas('0x...', { limit: 50 });
for (const entry of page.items) {
  console.log(entry.nonce, entry.status, entry.statusTimestamp);
  if (entry.category) {
    console.log('category:', entry.category);
    console.log('notes:', entry.noteCounts);
    if (entry.proposalType) {
      console.log('proposal type:', entry.proposalType);
    }
    if (entry.assets && entry.assets.length > 0) {
      for (const a of entry.assets) {
        console.log('asset:', a.assetId, a.amount);
      }
    }
  } else {
    // No dashboard fields → render as "metadata unavailable", not as
    // zero-valued defaults. Reasons listed above.
    console.log('metadata unavailable');
  }
}

For the full structured projection of a single delta (decoded input / output notes, vault changes, storage changes, plus the full proposal intent block), call getAccountDeltaDetail:

const detail = await client.getAccountDeltaDetail('0x...', entry.nonce);
console.log('input notes:', detail.inputNotes.length);
console.log('vault changes:', detail.vaultChanges);
if (detail.proposal) {
  console.log('proposal intent:', detail.proposal);
}

// Debug: include the raw base64 TransactionSummary blob in the
// response so you can sanity-check the server's projection against
// the on-disk bytes.
const debug = await client.getAccountDeltaDetail('0x...', entry.nonce, {
  includeRaw: true,
});
console.log('raw bytes (base64):', debug.rawTransactionSummary);

Per-Account In-Flight Proposals

Single-key Miden accounts and EVM accounts always return an empty proposal queue.

const page = await client.listAccountProposals('0x...', { limit: 50 });
for (const entry of page.items) {
  console.log(
    entry.commitment,
    entry.signaturesCollected,
    '/',
    entry.signaturesRequired,
  );
}

Pause / Unpause Account

Both endpoints require the accounts:pause permission and are idempotent. Re-pausing an already-paused account returns success with the original pausedAt and pausedReason preserved (first-writer wins). reason is required on pause (non-empty, ≤ 512 chars) and optional on unpause.

const paused = await client.pauseAccount('0x...', 'compliance review');
console.log(paused.pausedAt, paused.pausedReason);

await client.unpauseAccount('0x...', 'cleared by legal');

While an account is paused, mutating endpoints (delta submission, proposal creation, proposal signing) reject with HTTP 409 and code account_paused (normalized by the client from the server wire code GUARDIAN_ACCOUNT_PAUSED). Account setup paths (configureAccount, EVM register) are admin operations and remain available while paused. Read endpoints — including listAccounts({ paused: true }) and getAccount() — continue to work and surface pausedAt / pausedReason on the response.

Cross-Account Delta Feed

Paginated newest-first by status_timestamp DESC. The optional status filter accepts a single status or an array; the wrapper serializes the array to a comma-separated query parameter.

const page = await client.listGlobalDeltas({
  limit: 50,
  status: ['candidate', 'canonical'],
});
for (const entry of page.items) {
  console.log(entry.accountId, entry.nonce, entry.status);
}

Cross-Account In-Flight Proposal Feed

Paginated newest-first by originating_timestamp DESC. There is no status filter — every entry is in-flight by definition. EVM accounts do not appear in v1.

const page = await client.listGlobalProposals({ limit: 50 });
for (const entry of page.items) {
  console.log(
    entry.accountId,
    entry.commitment,
    entry.signaturesCollected,
    '/',
    entry.signaturesRequired,
  );
}

Logout

await client.logout();

Pagination Shape

Every paginated dashboard endpoint returns a PagedResult<T> envelope:

{
  "items": [ /* T entries */ ],
  "next_cursor": "string | null"  // null at end of list
}
  • limit defaults to 50 and is capped at 500. A bare ?limit= (present but empty) is treated as omitted; out-of-range values return 400 invalid_limit.
  • cursor is opaque, server-signed, and tied to a specific endpoint kind (e.g. an account-list cursor cannot be replayed on the deltas endpoint). Tampered or stale cursors return 400 invalid_cursor. There is no client-visible TTL.
  • A cursor may be supplied without a limit; the default of 50 applies.
  • The account list endpoint sorts by (updated_at DESC, account_id ASC). Concurrent updates to updated_at MAY cause an account to be skipped or repeated across a traversal — documented expected behavior of cursor pagination over a mutable sort key. All other paginated endpoints sort by an immutable per-account nonce (or (nonce, commitment) for proposals) and are fully stable.

Cookie Transport

The Guardian operator session is cookie-based. This package does not manage a cookie jar. Configure credentials or a custom fetch implementation appropriate for your runtime.

Error Handling

import {
  GuardianOperatorContractError,
  GuardianOperatorHttpError,
  isDashboardErrorCode,
} from '@openzeppelin/guardian-operator-client';

try {
  await client.listAccounts({ limit: 9999 });
} catch (error) {
  if (error instanceof GuardianOperatorHttpError) {
    // Stable machine-readable code lives on `error.data.code`.
    // Branch on it rather than on `error.status` or
    // `error.data.error` (the human message).
    const code = error.data?.code;
    if (code && isDashboardErrorCode(code)) {
      switch (code) {
        case 'invalid_limit':
        case 'invalid_cursor':
        case 'invalid_status_filter':
          // Caller bug — fix the request.
          break;
        case 'account_not_found':
          // Path-addressed account does not exist.
          break;
        case 'data_unavailable':
          // Metadata exists but storage cannot be read; retry later.
          break;
        case 'authentication_failed':
          // Operator session is missing, tampered, or expired —
          // re-issue the challenge / verify flow.
          break;
      }
    }
  }

  if (error instanceof GuardianOperatorContractError) {
    console.error(error.message);
  }
}

The dashboard error taxonomy (feature 005-operator-dashboard-metrics FR-028) is:

| HTTP | Body code | When | |------|-------------|------| | 401 | authentication_failed | missing / tampered / expired operator session | | 403 | GUARDIAN_INSUFFICIENT_OPERATOR_PERMISSION | operator session is valid but lacks one or more route-required permissions (feature 006-operator-authz) | | 404 | account_not_found | path-addressed account does not exist | | 400 | invalid_cursor | tampered, malformed, or stale cursor | | 400 | invalid_limit | limit outside [1, 500] | | 400 | invalid_status_filter | global delta feed status filter is unknown or malformed | | 409 | account_paused | mutating call against a paused account; error details carry pausedAt and pausedReason. Wire code GUARDIAN_ACCOUNT_PAUSED is normalized by the client. | | 503 | data_unavailable | metadata exists but underlying records cannot be read |

Operator Authorization (feature 006-operator-authz)

The dashboard now enforces per-operator permissions on every route. The allowlist JSON consumed by GUARDIAN_OPERATOR_PUBLIC_KEYS_JSON / _FILE / _SECRET_ID accepts a heterogeneous array of two element shapes:

[
  // Legacy element: bare hex string → `{dashboard:read}` only.
  "0x094f145ec43583db3ca443f43a67545c...",

  // Structured element: explicit permission set.
  {
    "public_key": "0x0944089753530e9104cc9fc4...",
    "permissions": ["dashboard:read", "accounts:pause"]
  },

  // Explicit-deny element: loads, but denies every authorization
  // check (different from omitting the operator entirely).
  {
    "public_key": "0x0a11...",
    "permissions": []
  }
]

Recognized permissions in v1: dashboard:read, accounts:pause, policies:write.

When the server denies a request for insufficient permissions, the response body extends the existing flat envelope additively:

{
  "success": false,
  "code": "GUARDIAN_INSUFFICIENT_OPERATOR_PERMISSION",
  "error": "Operator lacks required permissions: accounts:pause",
  "missing_permissions": ["accounts:pause"],
  "retryable": false
}

parseErrorBody and the GuardianOperatorHttpErrorData shape both expose missingPermissions and retryable for this code; the fields are undefined for every other code.

For UI gating, call getSession() to read the authenticated operator's live permission set and compare entries against the exported wire-string constants:

import {
  ACCOUNTS_PAUSE,
  DASHBOARD_READ,
  GuardianOperatorHttpClient,
} from '@openzeppelin/guardian-operator-client';

const client = new GuardianOperatorHttpClient('https://guardian.example');
const { permissions } = await client.getSession();

const canPause = permissions.includes(ACCOUNTS_PAUSE);
const canRead = permissions.includes(DASHBOARD_READ);

The session endpoint re-reads permissions from the allowlist on every call, so allowlist edits take effect without re-login. The server is still the source of truth on every mutating endpoint — the client MUST NOT short-circuit a request based on its own capability check.